Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions doc/changes/changelog.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions doc/changes/changes_0.2.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# 0.2.0 - 2026-09-15

## Summary

Major refactoring of client API. Support for global disable of telemetry and verbose mode.

## Refactoring

- #4: Support v0.2 of the protocol + API refactor
- #8: Provide py.typed marker
42 changes: 12 additions & 30 deletions doc/client-python.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,54 +10,36 @@ public repo yet) ``pip install exasol-telemetry-client``
Usage
-----

Once installed, package ``exasel.telemetry.client`` provides three
methods and one exception: - ``setup()``: configures the library, has to
be called once in the beginning - ``track(feature_name)``: tracks
feature as used (string) - ``shutdown()``: should be called at the end
of the program. If not called, some tracked features could be lost. -
``TelemetryError``: exception could be thrown during ``setup()`` call,
if environment variables are wrong.
Once installed, package ``exasol.telemetry.client`` provides three
methods:

Function ``was_setup()`` could be used to check the ``setup()`` was called
before (possibly in another library).
- ``track(product_name, product_version, feature_name)``: tracks feature as used (string)
- ``shutdown()``: should be called at the end of the program. If not called, some tracked features could be lost.
- ``disable()``: disables telemetry entirely for the whole process till the termination of the process. It is useful for cases when some software wants to disable telemetry even when some of its libraries are using it.

Explicit initialization of the library is not needed --- it will be set up on the first ``track()`` call.

Example of minimalistic program:

.. code:: python

import logging
from exasol.telemetry.client import *

if __name__ == "__main__":
try:
try:
if not was_setup():
setup()
except TelemetryError as e:
logging.warning("Telemetry disabled due to error: %s", str(e))

track("feature1")
track("feature2")
track("hello-world", "0.1", "started")
core_of_the_program()
finally:
shutdown()

Exceptions
----------

Exception ``TelemetryError`` could be thrown from ``setup()`` and
``shutdown()`` in case of errors. Call of ``track()`` never raises exceptions, in case of errors
tracked feature is ignored.

Environment variables
---------------------

To change the telemetry configuration, you can set the following
environment variables. Those values also could be changed via
``setup()`` arguments.
environment variables.

- ``EXASOL_TELEMETRY_DISABLE`` - any value disables the telemetry data
collection and sending
- ``EXASOL_TELEMETRY_ENDPOINT`` - redefines telemetry endpoint url.

In addition, if environment variable ``CI=true`` (which is the case during Github CI workflows run)
the telemetry is disabled unless explicitly enabled with ``setup()`` arguments.
- ``EXASOL_TELEMETRY_VERBOSE`` -- enables logging messages from the library. Could be used to make sure integration was done properly.
- ``CI=true`` -- disables telemetry to prevent tracking during CI.
1 change: 1 addition & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Documentation of telemetry

user_guide
client-python
protocol
developer_guide
api
faq
Expand Down
52 changes: 52 additions & 0 deletions doc/protocol.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
Telemetry Protocol Specification
================================

Exasol telemetry uses simplistic protocol sending events happened in the the software.
Every event has a timestamp attached to be used for server-side analytics.
All the data on the server is immediately aggregated and anonymized and no personal information
is transferred or stored.

The data is transferred in json format and at the moment there are two versions of the protocol.

Version 0.1
-----------
.. code:: json

{
"version": "0.1",
"timestamp": 1787036195,
"features": {
"EMCP.started": [1787036195]
}
}

Transferred data has the following fields:

- ``version``: string specifying the protocol version
- ``timestamp``: UTC timestamp of the transmission attempt
- ``features``: dictionary with pairs ``feature-name`` and vector of timestamps when the event happened.

Recording of both event timestamp and transmission timestamp allows to check the clock discrepancies on the client
side and filter out outliers.

Version 0.2
-----------

This is an extension of version 0.1, sample data is below ::

{
"version": "0.2",
"category": "EMCP",
"productVersion": "0.22",
"timestamp": 1787036195,
"features": {
"started": [1787036195]
}
}

In this version we have two new top-level fields added:

- ``category``: name of the product
- ``productVersion``: version of the product

The name of the product is no longer prepended to the features, which makes the data more compact.
12 changes: 4 additions & 8 deletions exasol/telemetry/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,15 @@
Telemetry client library for python.

Public API is three methods:
- setup: initializes the library based on explicit arguments or environment variables
- shutdown: cleans up the resources and sends the data still in buffers
- track: remembers the feature name in the buffer (will be sent in background)

On error we raise exception TelemetryError with error message.
- disable: disables the telemetry
- shutdown: cleans up the resources and sends the data still in buffers
"""

from exasol.telemetry.client.config import was_setup
from exasol.telemetry.client.errors import TelemetryError
from exasol.telemetry.client.setup import (
setup,
disable,
shutdown,
)
from exasol.telemetry.client.worker import track

__all__ = ["was_setup", "setup", "shutdown", "track", "TelemetryError"]
__all__ = ["track", "disable", "shutdown"]
10 changes: 10 additions & 0 deletions exasol/telemetry/client/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
ENV_DISABLE = "EXASOL_TELEMETRY_DISABLE"
# Endpoint (has to be valid http/https URL)
ENV_ENDPOINT = "EXASOL_TELEMETRY_ENDPOINT"
# Enable console logging of telemetry events
ENV_VERBOSE = "EXASOL_TELEMETRY_VERBOSE"
# GitHub CI sets this to true during execution
ENV_CI = "CI"

Expand Down Expand Up @@ -49,3 +51,11 @@ def was_enabled() -> bool:
"""
conf = get()
return conf is not None and conf.enabled


def disable_config():
"""
Call disables telemetry entirely for all subsequent calls.
"""
conf = Config(enabled=False, endpoint=DEFAULT_ENDPOINT)
store(conf)
7 changes: 0 additions & 7 deletions exasol/telemetry/client/errors.py

This file was deleted.

25 changes: 22 additions & 3 deletions exasol/telemetry/client/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
import typing as tt

# Version of the protocol
VERSION = "0.1"
VERSION = "0.2"

# Name of the feature
# type aliases
Feature = str
ProductName = str
ProductVersion = str

# Timestamp of the measurement
Timestamp = tt.Union[int, float]
Expand All @@ -28,6 +30,12 @@ class Message:
# Version of the protocol this message corresponds to.
version: str

# Name of the product ('category' in v0.2 protocol)
product_name: ProductName

# Version of the product
product_version: ProductVersion

# Current unit timestamp when the message was created
# (used by the server to get the age of the individual reports)
timestamp: Timestamp
Expand All @@ -38,20 +46,31 @@ class Message:
def to_json(self) -> dict:
return {
"version": self.version,
"category": self.product_name,
"productVersion": self.product_version,
"timestamp": self.timestamp,
"features": self.features,
}

@classmethod
def from_features(cls, features: Features) -> "Message":
def from_features(
cls,
product_name: ProductName,
product_version: ProductVersion,
features: Features,
) -> "Message":
"""
Construct the message object from collected features.
We're not deep copy of features, just store the reference of it.
:param features: collection of features
:param product_name: name of the product
:param product_version: version of the product
:return: Message created
"""
return Message(
version=VERSION,
product_name=product_name,
product_version=product_version,
timestamp=get_current_ts(),
features=features,
)
Expand Down
Empty file.
35 changes: 27 additions & 8 deletions exasol/telemetry/client/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

from exasol.telemetry.client import (
config,
verbose,
worker,
)
from exasol.telemetry.client.errors import TelemetryError


def get_value(
Expand Down Expand Up @@ -42,10 +42,19 @@ def is_valid_endpoint_url(url: str) -> bool:
return res.scheme in ("http", "https") and len(res.netloc) > 0


def setup_verbose_if_needed():
"""
Function enables verbose mode for telemetry prefix if env variable is set.
"""
# if env variable is not set, do nothing
if get_value(None, config.ENV_VERBOSE, None) is None:
return
verbose.setup_logging()


def setup(endpoint: tt.Optional[str] = None, disable: tt.Optional[bool] = None) -> bool:
"""
Telemetry client setup function. Has to be called before any other
calls to the client.
Telemetry client setup function.

Explicitly given arguments have the highest priority.
If they are not given, we check the environment variables (EXASOL_TELEMETRY_XXX),
Expand All @@ -58,7 +67,6 @@ def setup(endpoint: tt.Optional[str] = None, disable: tt.Optional[bool] = None)
:param disable: If True, disable telemetry communication and
data accumulation.

:raises TelemetryError: if error has happened
:returns True if telemetry is active according to configuration,
False if it was disabled
"""
Expand All @@ -85,11 +93,14 @@ def setup(endpoint: tt.Optional[str] = None, disable: tt.Optional[bool] = None)
enabled = not val_disabled

if enabled and not is_valid_endpoint_url(val_endpoint):
raise TelemetryError("Endpoint is invalid: " + val_endpoint)
enabled = False

conf = config.Config(endpoint=val_endpoint, enabled=enabled)
config.store(conf)
worker.start_worker()
if enabled:
setup_verbose_if_needed()
worker.start_worker()
verbose.log("Setup is done, enabled=%s", conf.enabled)
return conf.enabled


Expand All @@ -101,6 +112,14 @@ def shutdown(flush_buffers: bool = True):
so some values might be lost.
"""
if not config.was_setup():
raise TelemetryError("Telemetry was not initialized")
return
verbose.log("Shutdown")
worker.stop_worker(flush_buffers)
config.store(None)


def disable():
"""
Shuts down workers and disables the telemetry globally.
"""
config.disable_config()
worker.stop_worker(flush_buffers=False)
25 changes: 25 additions & 0 deletions exasol/telemetry/client/verbose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import logging
import typing as tt

LOGGER = "exasol.telemetry.client"
LEVEL = logging.DEBUG

logger: tt.Optional[logging.Logger] = None


def setup_logging():
"""
Enable logging for our package.
"""
global logger
# prevent double-initialization
if logger is not None:
return
logger = logging.getLogger(LOGGER)
logger.setLevel(LEVEL)


def log(msg: str, *args, **kwargs):
global logger
if logger is not None:
logger.log(LEVEL, msg, *args, **kwargs)
Loading