Common telemetry (#5032)

* Moved telemetry to repo root directory from MO

* New telemetry package in the "openvino" sub-directory

* Removed telemetry from the MO BOM

* Updated MO BOM and added stub file for telemetry

* Fixed license header

* Fixed license headers and cleaned up the telemetry setup.py

* Fixed import

* Added temporary dependency for openvino-telemetry

* Added ignore for pylint issues

* Fixed import statements

* Updated imports in the telemetry library

* Removed telemetry library. Added link to another private repo

* Removed redundant start_session event for the MO

* Changed approach to import the telemetry library

* Minor code refactoring

* Updated MO telemetry events sending messages

* Refactor sending events for the IE runtime check

* Disable forcing warnings for deprecated methods

* Removed changes from the requirements.txt to install telemetry library to avoid merge conflicts

* Update copyright in the model-optimizer/mo/utils/telemetry_stub.py

Co-authored-by: Gleb Kazantaev <gleb.nnstu@gmail.com>

Co-authored-by: Gleb Kazantaev <gleb.nnstu@gmail.com>
This commit is contained in:
Evgeny Lazarev 2021-05-14 21:56:03 +03:00 committed by GitHub
parent c500f0a783
commit 717499cf2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 64 additions and 516 deletions

View File

@ -1072,6 +1072,7 @@ mo/utils/shape.py
mo/utils/simple_proto_parser.py
mo/utils/str_to.py
mo/utils/summarize_graph.py
mo/utils/telemetry_stub.py
mo/utils/tensorboard_util.py
mo/utils/unsupported_ops.py
mo/utils/utils.py
@ -1089,13 +1090,3 @@ requirements_mxnet.txt
requirements_onnx.txt
requirements_tf.txt
requirements_tf2.txt
telemetry/__init__.py
telemetry/backend/__init__.py
telemetry/backend/backend.py
telemetry/backend/backend_ga.py
telemetry/telemetry.py
telemetry/utils/__init__.py
telemetry/utils/guid.py
telemetry/utils/isip.py
telemetry/utils/message.py
telemetry/utils/sender.py

View File

@ -9,7 +9,7 @@ from mo.utils.cli_parser import parse_transform
def get_available_transformations():
try:
from openvino.offline_transformations import ApplyLowLatencyTransformation # pylint: disable=import-error
from openvino.offline_transformations import ApplyLowLatencyTransformation # pylint: disable=import-error,no-name-in-module
return {
'LowLatency': ApplyLowLatencyTransformation,
}
@ -22,8 +22,8 @@ def apply_offline_transformations(input_model: str, framework: str, transforms:
# to produce correct mapping
extract_names = framework in ['tf', 'mxnet', 'kaldi']
from openvino.inference_engine import read_network # pylint: disable=import-error
from openvino.offline_transformations import ApplyMOCTransformations, GenerateMappingFile # pylint: disable=import-error
from openvino.inference_engine import read_network # pylint: disable=import-error,no-name-in-module
from openvino.offline_transformations import ApplyMOCTransformations, GenerateMappingFile # pylint: disable=import-error,no-name-in-module
net = read_network(input_model + "_tmp.xml", input_model + "_tmp.bin")

View File

@ -6,15 +6,19 @@ import datetime
import logging as log
import os
import platform
import sys
import subprocess
import sys
import traceback
from collections import OrderedDict
from copy import deepcopy
import numpy as np
import telemetry.telemetry as tm
try:
import openvino_telemetry as tm
except ImportError:
import mo.utils.telemetry_stub as tm
from extensions.back.SpecialNodesFinalization import RemoveConstOps, CreateConstNodesReplacement, NormalizeTI
from mo.back.ie_ir_ver_2.emitter import append_ir_info
from mo.graph.graph import Graph
@ -107,7 +111,6 @@ def prepare_ir(argv: argparse.Namespace):
log.debug(str(argv))
log.debug("Model Optimizer started")
t = tm.Telemetry()
t.start_session()
model_name = "<UNKNOWN_NAME>"
if argv.model_name:
@ -373,7 +376,7 @@ def driver(argv: argparse.Namespace):
def main(cli_parser: argparse.ArgumentParser, framework: str):
telemetry = tm.Telemetry(app_name='Model Optimizer', app_version=get_simplified_mo_version())
telemetry.start_session()
telemetry.start_session('mo')
telemetry.send_event('mo', 'version', get_simplified_mo_version())
try:
# Initialize logger with 'ERROR' as default level to be able to form nice messages
@ -391,7 +394,7 @@ def main(cli_parser: argparse.ArgumentParser, framework: str):
if ov_update_message:
print(ov_update_message)
telemetry.send_event('mo', 'conversion_result', 'success')
telemetry.end_session()
telemetry.end_session('mo')
telemetry.force_shutdown(1.0)
return ret_code
except (FileNotFoundError, NotADirectoryError) as e:
@ -418,7 +421,7 @@ def main(cli_parser: argparse.ArgumentParser, framework: str):
log.error("-------------------------------------------------")
telemetry.send_event('mo', 'conversion_result', 'fail')
telemetry.end_session()
telemetry.end_session('mo')
telemetry.force_shutdown(1.0)
return 1

View File

@ -3,12 +3,10 @@
# Copyright (C) 2018-2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
import os
import re
import sys
import argparse
import os
import platform
import sys
try:
import mo
@ -19,15 +17,22 @@ except ModuleNotFoundError:
execution_type = "install_prerequisites.{}".format("bat" if platform.system() == "Windows" else "sh")
import mo.utils.version as v
import telemetry.telemetry as tm
try:
import openvino_telemetry as tm # pylint: disable=import-error,no-name-in-module
except ImportError:
import mo.utils.telemetry_stub as tm
from mo.utils.error import classify_error_type
def send_telemetry(mo_version: str, message: str, event_type: str):
t = tm.Telemetry(app_name='Model Optimizer', app_version=mo_version)
t.start_session()
t = tm.Telemetry(app_name='Version Checker', app_version=mo_version)
# do not trigger new session if we are executing from the check from within the MO because it is actually not model
# conversion run which we want to send
if execution_type != 'mo':
t.start_session(execution_type)
t.send_event(execution_type, event_type, message)
t.end_session()
if execution_type != "mo":
t.end_session(execution_type)
t.force_shutdown(1.0)
@ -42,16 +47,17 @@ def import_core_modules(silent: bool, path_to_module: str):
:return: True if all imports were successful and False otherwise
"""
try:
from openvino.inference_engine import get_version, read_network # pylint: disable=import-error
from openvino.offline_transformations import ApplyMOCTransformations, ApplyLowLatencyTransformation, GenerateMappingFile # pylint: disable=import-error
from openvino.inference_engine import get_version, read_network # pylint: disable=import-error,no-name-in-module
from openvino.offline_transformations import ApplyMOCTransformations, ApplyLowLatencyTransformation, \
GenerateMappingFile # pylint: disable=import-error,no-name-in-module
import openvino # pylint: disable=import-error
import openvino # pylint: disable=import-error,no-name-in-module
if silent:
return True
ie_version = str(get_version())
mo_version = str(v.get_version()) # pylint: disable=no-member
mo_version = str(v.get_version()) # pylint: disable=no-member,no-name-in-module
print("\t- {}: \t{}".format("Inference Engine found in", os.path.dirname(openvino.__file__)))
print("{}: \t{}".format("Inference Engine version", ie_version))
@ -64,7 +70,8 @@ def import_core_modules(silent: bool, path_to_module: str):
mo_is_custom = extracted_mo_release_version == (None, None)
print("[ WARNING ] Model Optimizer and Inference Engine versions do no match.")
print("[ WARNING ] Consider building the Inference Engine Python API from sources or reinstall OpenVINO (TM) toolkit using", end=" ")
print("[ WARNING ] Consider building the Inference Engine Python API from sources or reinstall OpenVINO "
"(TM) toolkit using", end=" ")
if mo_is_custom:
print("\"pip install openvino\" (may be incompatible with the current Model Optimizer version)")
else:

View File

@ -3,7 +3,7 @@
def get_ie_version():
try:
from openvino.inference_engine import get_version # pylint: disable=import-error
from openvino.inference_engine import get_version # pylint: disable=import-error,no-name-in-module
return get_version()
except:
return None

View File

@ -0,0 +1,29 @@
# Copyright (C) 2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
class Telemetry(object):
"""
Stab file for the Telemetry class which is used when Telemetry class is not available.
"""
def __init__(self, *arg, **kwargs):
pass
def send_event(self, *arg, **kwargs):
pass
def send_error(self, *arg, **kwargs):
pass
def start_session(self, *arg, **kwargs):
pass
def end_session(self, *arg, **kwargs):
pass
def force_shutdown(self, *arg, **kwargs):
pass
def send_stack_trace(self, *arg, **kwargs):
pass

View File

@ -44,14 +44,12 @@ def deprecated_api(class_name=None, new_method_name=None):
def deprecated(func):
@functools.wraps(func)
def deprecation_message(*args, **kwargs):
warnings.simplefilter('always', DeprecationWarning) # turn on filter
dep_msg = "Call to deprecated function {}. ".format(func.__name__)
if class_name is not None:
dep_msg += "Please use {}.{} method" \
"".format(class_name.__name__ if not isinstance(class_name, str) else class_name,
func.__name__ if new_method_name is None else new_method_name)
warnings.warn(dep_msg, DeprecationWarning, stacklevel=2)
warnings.simplefilter('default', DeprecationWarning) # reset filter
return func(*args, **kwargs)
return deprecation_message

View File

@ -1,3 +0,0 @@
# Copyright (C) 2018-2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

View File

@ -1,4 +0,0 @@
# Copyright (C) 2018-2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
from .backend_ga import *

View File

@ -1,81 +0,0 @@
# Copyright (C) 2018-2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
import abc
from telemetry.utils.message import Message
class BackendRegistry:
"""
The class that stores information about all registered telemetry backends
"""
r = {}
@classmethod
def register_backend(cls, id: str, backend):
cls.r[id] = backend
@classmethod
def get_backend(cls, id: str):
assert id in cls.r, 'The backend with id "{}" is not registered'.format(id)
return cls.r.get(id)
class TelemetryBackendMetaClass(abc.ABCMeta):
def __init__(cls, clsname, bases, methods):
super().__init__(clsname, bases, methods)
if cls.id is not None:
BackendRegistry.register_backend(cls.id, cls)
class TelemetryBackend(metaclass=TelemetryBackendMetaClass):
id = None
@abc.abstractmethod
def __init__(self, tid: str, app_name: str, app_version: str):
"""
Initializer of the class
:param tid: database id
:param app_name: name of the application
:param app_version: version of the application
"""
@abc.abstractmethod
def send(self, message: Message):
"""
Sends the message to the backend.
:param message: The Message object to send
:return: None
"""
@abc.abstractmethod
def build_event_message(self, event_category: str, event_action: str, event_label: str, event_value: int = 1,
**kwargs):
"""
Should return the Message object build from the event message.
"""
@abc.abstractmethod
def build_error_message(self, error_msg: str, **kwargs):
"""
Should return the Message object build from the error message.
"""
@abc.abstractmethod
def build_stack_trace_message(self, error_msg: str, **kwargs):
"""
Should return the Message object build from the stack trace message.
"""
@abc.abstractmethod
def build_session_start_message(self, **kwargs):
"""
Should return the Message object corresponding to the session start.
"""
@abc.abstractmethod
def build_session_end_message(self, **kwargs):
"""
Should return the Message object corresponding to the session end.
"""

View File

@ -1,87 +0,0 @@
# Copyright (C) 2018-2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
import uuid
from telemetry.backend.backend import TelemetryBackend
from telemetry.utils.message import Message, MessageType
from telemetry.utils.guid import get_or_generate_uid
class GABackend(TelemetryBackend):
backend_url = 'https://www.google-analytics.com/collect'
id = 'ga'
def __init__(self, tid: str = None, app_name: str = None, app_version: str = None):
super(GABackend, self).__init__(tid, app_name, app_version)
if tid is None:
tid = 'UA-17808594-29'
self.tid = tid
self.uid = get_or_generate_uid('openvino_ga_uid', lambda: str(uuid.uuid4()), is_valid_uuid4)
self.app_name = app_name
self.app_version = app_version
self.default_message_attrs = {
'v': '1', # API Version
'tid': self.tid,
'cid': self.uid,
'an': self.app_name,
'av': self.app_version,
'ua': 'Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14' # dummy identifier of the browser
}
def send(self, message: Message):
try:
import requests
requests.post(self.backend_url, message.attrs, timeout=1.0)
except Exception:
pass
def build_event_message(self, event_category: str, event_action: str, event_label: str, event_value: int = 1,
**kwargs):
data = self.default_message_attrs.copy()
data.update({
't': 'event',
'ec': event_category,
'ea': event_action,
'el': event_label,
'ev': event_value,
})
return Message(MessageType.EVENT, data)
def build_session_start_message(self, **kwargs):
data = self.default_message_attrs.copy()
data.update({
'sc': 'start',
't': 'event',
'ec': 'session',
'ea': 'control',
'el': 'start',
'ev': 1,
})
return Message(MessageType.SESSION_START, data)
def build_session_end_message(self, **kwargs):
data = self.default_message_attrs.copy()
data.update({
'sc': 'end',
't': 'event',
'ec': 'session',
'ea': 'control',
'el': 'end',
'ev': 1,
})
return Message(MessageType.SESSION_END, data)
def build_error_message(self, error_msg: str, **kwargs):
pass
def build_stack_trace_message(self, error_msg: str, **kwargs):
pass
def is_valid_uuid4(uid: str):
try:
uuid.UUID(uid, version=4)
except ValueError:
return False
return True

View File

@ -1,91 +0,0 @@
# Copyright (C) 2018-2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
import telemetry.utils.isip as isip
from telemetry.backend.backend import BackendRegistry
from telemetry.utils.sender import TelemetrySender
class SingletonMetaClass(type):
def __init__(self, cls_name, super_classes, dic):
self.__single_instance = None
super().__init__(cls_name, super_classes, dic)
def __call__(cls, *args, **kwargs):
if cls.__single_instance is None:
cls.__single_instance = super(SingletonMetaClass, cls).__call__(*args, **kwargs)
return cls.__single_instance
class Telemetry(metaclass=SingletonMetaClass):
"""
The main class to send telemetry data. It uses singleton pattern. The instance should be initialized with the
application name, version and tracking id just once. Later the instance can be created without parameters.
"""
def __init__(self, app_name: str = None, app_version: str = None, tid: [None, str] = None,
backend: [str, None] = 'ga'):
if not hasattr(self, 'tid'):
self.tid = None
if app_name is not None:
self.consent = isip.isip_consent() == isip.ISIPConsent.APPROVED
# override default tid
if tid is not None:
self.tid = tid
self.backend = BackendRegistry.get_backend(backend)(self.tid, app_name, app_version)
self.sender = TelemetrySender()
else: # use already configured instance
assert self.sender is not None, 'The first instantiation of the Telemetry should be done with the ' \
'application name and version'
def force_shutdown(self, timeout: float = 1.0):
"""
Stops currently running threads which may be hanging because of no Internet connection.
:param timeout: maximum timeout time
:return: None
"""
self.sender.force_shutdown(timeout)
def send_event(self, event_category: str, event_action: str, event_label: str, event_value: int = 1, **kwargs):
"""
Send single event.
:param event_category: category of the event
:param event_action: action of the event
:param event_label: the label associated with the action
:param event_value: the integer value corresponding to this label
:param kwargs: additional parameters
:return: None
"""
if self.consent:
self.sender.send(self.backend, self.backend.build_event_message(event_category, event_action, event_label,
event_value, **kwargs))
def start_session(self, **kwargs):
"""
Sends a message about starting of a new session.
:param kwargs: additional parameters
:return: None
"""
if self.consent:
self.sender.send(self.backend, self.backend.build_session_start_message(**kwargs))
def end_session(self, **kwargs):
"""
Sends a message about ending of the current session.
:param kwargs: additional parameters
:return: None
"""
if self.consent:
self.sender.send(self.backend, self.backend.build_session_end_message(**kwargs))
def send_error(self, error_msg: str, **kwargs):
if self.consent:
pass
def send_stack_trace(self, stack_trace: str, **kwargs):
if self.consent:
pass

View File

@ -1,3 +0,0 @@
# Copyright (C) 2018-2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

View File

@ -1,64 +0,0 @@
# Copyright (C) 2018-2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
import os
from platform import system
import telemetry.utils.isip as isip
def save_uid_to_file(file_name: str, uid: str):
"""
Save the uid to the specified file
"""
try:
# create directories recursively first
os.makedirs(os.path.dirname(file_name), exist_ok=True)
with open(file_name, 'w') as file:
file.write(uid)
except Exception as e:
print('Failed to generate the UID file: {}'.format(str(e)))
return False
return True
def get_or_generate_uid(file_name: str, generator: callable, validator: [callable, None]):
"""
Get existing UID or generate a new one.
:param file_name: name of the file with the UID
:param generator: the function to generate the UID
:param validator: the function to validate the UID
:return: existing or a new UID file
"""
full_path = os.path.join(get_uid_path(), file_name)
uid = None
if os.path.exists(full_path):
with open(full_path, 'r') as file:
uid = file.readline().strip()
if uid is not None and (validator is not None and not validator(uid)):
uid = None
if uid is None:
uid = generator()
save_uid_to_file(full_path, uid)
return uid
def get_uid_path():
"""
Returns a directory with the the OpenVINO randomly generated UUID file.
:return: the directory with the the UUID file
"""
platform = system()
subdir = None
if platform == 'Windows':
subdir = 'Intel Corporation'
elif platform in ['Linux', 'Darwin']:
subdir = '.intel'
if subdir is None:
raise Exception('Failed to determine the operation system type')
return os.path.join(isip.isip_consent_base_dir(), subdir)

View File

@ -1,70 +0,0 @@
# Copyright (C) 2018-2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
import os
from enum import Enum
from platform import system
class ISIPConsent(Enum):
APPROVED = 0
DECLINED = 1
UNKNOWN = 2
def isip_consent_base_dir():
"""
Returns the base directory with the ISIP consent file. The full directory may not have write access on Linux/OSX
systems so that is why the base directory is used.
:return:
"""
platform = system()
dir_to_check = None
if platform == 'Windows':
dir_to_check = '$LOCALAPPDATA'
elif platform in ['Linux', 'Darwin']:
dir_to_check = '$HOME'
if dir_to_check is None:
raise Exception('Failed to find location of the ISIP consent')
return os.path.expandvars(dir_to_check)
def _isip_consent_sub_directory():
platform = system()
if platform == 'Windows':
return 'Intel Corporation'
elif platform in ['Linux', 'Darwin']:
return 'intel'
raise Exception('Failed to find location of the ISIP consent')
def _isip_consent_dir():
dir_to_check = os.path.join(isip_consent_base_dir(), _isip_consent_sub_directory())
return os.path.expandvars(dir_to_check)
def _isip_consent_file():
return os.path.join(_isip_consent_dir(), 'isip')
def isip_consent():
file_to_check = _isip_consent_file()
if not os.path.exists(file_to_check):
return ISIPConsent.UNKNOWN
try:
with open(file_to_check, 'r') as file:
content = file.readline().strip()
if content == '1':
return ISIPConsent.APPROVED
else:
return ISIPConsent.DECLINED
except Exception as e:
pass
# unknown value in the file is considered as a unknown consent
return ISIPConsent.UNKNOWN

View File

@ -1,18 +0,0 @@
# Copyright (C) 2018-2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
from enum import Enum
class MessageType(Enum):
EVENT = 0
ERROR = 1
STACK_TRACE = 2
SESSION_START = 3
SESSION_END = 4
class Message:
def __init__(self, type: MessageType, attrs: dict):
self.type = type
self.attrs = attrs.copy()

View File

@ -1,59 +0,0 @@
# Copyright (C) 2018-2021 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
import threading
from concurrent import futures
from time import sleep
from telemetry.backend.backend import TelemetryBackend
from telemetry.utils.message import Message
MAX_QUEUE_SIZE = 1000
class TelemetrySender:
def __init__(self, max_workers=None):
self.executor = futures.ThreadPoolExecutor(max_workers=max_workers)
self.queue_size = 0
self._lock = threading.Lock()
def send(self, backend: TelemetryBackend, message: Message):
def _future_callback(future):
with self._lock:
self.queue_size -= 1
free_space = False
with self._lock:
if self.queue_size < MAX_QUEUE_SIZE:
free_space = True
self.queue_size += 1
else:
pass # dropping a message because the queue is full
# to avoid dead lock we should not add callback inside the "with self._lock" block because it will be executed
# immediately if the fut is available
if free_space:
fut = self.executor.submit(backend.send, message)
fut.add_done_callback(_future_callback)
def force_shutdown(self, timeout: float):
"""
Forces all threads to be stopped after timeout. The "shutdown" method of the ThreadPoolExecutor removes only not
yet scheduled threads and keep running the existing one. In order to stop the already running use some low-level
attribute. The operation with low-level attributes is wrapped with the try/except to avoid potential crash if
these attributes will removed or renamed.
:param timeout: timeout to wait before the shutdown
:return: None
"""
try:
need_sleep = False
with self._lock:
if self.queue_size > 0:
need_sleep = True
if need_sleep:
sleep(timeout)
self.executor.shutdown(wait=False)
self.executor._threads.clear()
futures.thread._threads_queues.clear()
except Exception:
pass

View File

@ -16,7 +16,7 @@ full_name_patterns_to_skip = ['^mo/utils/convert.py$',
]
if platform.system() == 'Windows':
full_name_patterns_to_skip = [i.replace('/', '\\\\') for i in full_name_patterns_to_skip]
dirs_to_search = ['mo', 'extensions', 'tf_call_ie_layer', 'telemetry']
dirs_to_search = ['mo', 'extensions']
def is_match(name: str, patterns: ()):