diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b8f33a5..b5e280cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,7 @@ jobs: env: PYTHONUTF8: 1 PYTHONIOENCODING: utf-8 + BABYLON_DISABLE_TELEMETRY: "true" steps: - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v7 diff --git a/Babylon/commands/macro/init.py b/Babylon/commands/macro/init.py index af672f7a..db5d3f6e 100644 --- a/Babylon/commands/macro/init.py +++ b/Babylon/commands/macro/init.py @@ -4,9 +4,11 @@ from shutil import copy from click import command, echo, option, style +from opentelemetry import trace from Babylon.utils.environment import Environment +tracer = trace.get_tracer(__name__) logger = getLogger(__name__) env = Environment() @@ -14,6 +16,7 @@ @command() @option("--project-folder", default="project", help="Name of the project folder to create (default: 'project').") @option("--variables-file", default="variables.yaml", help="Name of the variables file (default: 'variables.yaml').") +@tracer.start_as_current_span("macro_init_command") def init(project_folder: str, variables_file: str): """ Scaffolds a new Babylon project structure using YAML templates. diff --git a/Babylon/main.py b/Babylon/main.py index d4cd4364..b42fd85a 100644 --- a/Babylon/main.py +++ b/Babylon/main.py @@ -14,8 +14,11 @@ from Babylon.utils.dry_run import display_dry_run from Babylon.utils.environment import Environment from Babylon.utils.interactive import INTERACTIVE_ARG_VALUE, interactive_run +from Babylon.utils.telemetry import setup_telemetry from Babylon.version import VERSION +trace = setup_telemetry() +tracer = trace.get_tracer(__name__) logger = logging.getLogger() logging.getLogger("azure").setLevel(logging.WARNING) u_log = logging.getLogger("urllib3") diff --git a/Babylon/utils/decorators.py b/Babylon/utils/decorators.py index 6708c290..c1885d91 100644 --- a/Babylon/utils/decorators.py +++ b/Babylon/utils/decorators.py @@ -9,6 +9,7 @@ from click import Choice, ClickException, get_current_context, option from click import Path as ClickPath +from opentelemetry import trace from rich.console import Console from rich.padding import Padding from rich.syntax import Syntax @@ -19,6 +20,7 @@ from Babylon.utils.response import CommandResponse from Babylon.version import get_version +tracer = trace.get_tracer(__name__) logger = logging.getLogger("Babylon") env = Environment() console = Console() @@ -196,17 +198,19 @@ def wrap_function(func: Callable[..., Any]) -> Callable[..., Any]: ) @wraps(func) def wrapper(*args: Any, **kwargs: Any): - context = kwargs.pop("context", None) - if context and check_special_char(string=context): - env.set_context(context) - tenant = kwargs.pop("tenant", None) - if tenant and check_special_char(string=tenant): - env.set_environ(tenant) - state_id = kwargs.pop("state_id", None) - if state_id and check_special_char(string=state_id): - env.set_state_id(state_id) - env.get_namespace_from_local(context=context, tenant=tenant, state_id=state_id) - return func(*args, **kwargs) + span_name = f"{func.__module__}.{func.__qualname__}" + with tracer.start_as_current_span(span_name): + context = kwargs.pop("context", None) + if context and check_special_char(string=context): + env.set_context(context) + tenant = kwargs.pop("tenant", None) + if tenant and check_special_char(string=tenant): + env.set_environ(tenant) + state_id = kwargs.pop("state_id", None) + if state_id and check_special_char(string=state_id): + env.set_state_id(state_id) + env.get_namespace_from_local(context=context, tenant=tenant, state_id=state_id) + return func(*args, **kwargs) return wrapper diff --git a/Babylon/utils/telemetry.py b/Babylon/utils/telemetry.py new file mode 100644 index 00000000..f3280388 --- /dev/null +++ b/Babylon/utils/telemetry.py @@ -0,0 +1,58 @@ +from logging import getLogger +from os import getenv +from sys import platform + +from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.resources import OS_TYPE, SERVICE_NAME, SERVICE_VERSION, Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from requests import get + +from Babylon.version import VERSION + +logger = getLogger(__name__) + + +def setup_telemetry() -> trace.Tracer: + """Prepare telemetry configuration. Two environment variables are used to configure telemetry: + - BABYLON_DISABLE_TELEMETRY: If set to "true", telemetry is disabled. + - OTEL_EXPORTER_OTLP_ENDPOINT: Specifies the endpoint to which telemetry data is sent. + + Returns: + trace.Tracer: The configured tracer instance. + """ + + if getenv("BABYLON_DISABLE_TELEMETRY", "false").lower() == "true": + logger.info("Telemetry is disabled") + return trace + + endpoint = getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://telemetry.westeurope.azurecontainer.io:4318/v1/traces") + + try: + get(endpoint, timeout=2) + except Exception as e: + logger.warning(f"Telemetry endpoint {endpoint} is not reachable: {e}. Continuing without telemetry.") + return trace + + resource = Resource.create( + attributes={ + SERVICE_NAME: "Babylon", + SERVICE_VERSION: VERSION, + OS_TYPE: platform, + } + ) + + try: + otlp_exporter = OTLPSpanExporter( + endpoint=endpoint, + timeout=2, + ) + span_processor = BatchSpanProcessor(otlp_exporter, export_timeout_millis=5000) + trace.set_tracer_provider(TracerProvider(resource=resource)) + trace.get_tracer_provider().add_span_processor(span_processor) + except Exception as e: + logger.warning(f"Telemetry setup failed: {e}. Continuing without telemetry.") + trace.set_tracer_provider(TracerProvider()) + + return trace diff --git a/README.md b/README.md index f80ec036..7f96a561 100644 --- a/README.md +++ b/README.md @@ -76,4 +76,7 @@ To release a new version follow the checklist: - Generate lock file `uv lock` - Make a git tag, `git tag -a -m "5.0.0-beta.2" 5.0.0-beta.2` - Push the tag `git push tag 5.0.0-beta.2` -- Write release notes publish on [GitHub](https://github.com/Cosmo-Tech/Babylon/releases) \ No newline at end of file +- Write release notes publish on [GitHub](https://github.com/Cosmo-Tech/Babylon/releases) + +## Data Collection +Telemetry configuration is on by default, to opt out set the environment variable BABYLON_DISABLE_TELEMETRY to false. No personal identifiable information is collected. Our privacy statement is located at [Cosmo-Tech](https://cosmotech.com/privacy-policy/) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 73ca2c2c..465590e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,8 +23,12 @@ dependencies = [ "flatten_json", "dynaconf", "azure-mgmt-authorization>=4.0.0", + "azure-containerregistry>=1.2.0", "kubernetes>=35.0.0", - "cosmotech-api==5.0.0rc4" + "cosmotech-api==5.0.0rc4", + "opentelemetry-api", + "opentelemetry-sdk", + "opentelemetry-exporter-otlp" ] dynamic = ["version"] readme = {file = "README.md", content-type = "text/markdown"}