diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 402469fe..79b3f90d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: test: runs-on: ubuntu-latest env: - py39: 3.9 + py39: "3.9" py310: "3.10" py311: "3.11" py312: "3.12" @@ -82,4 +82,16 @@ jobs: python-version: ${{ env[matrix.python-version] }} architecture: "x64" - run: pip install -r dev-requirements.txt + - name: run recorded tests with python 3.10+ where urllib3 2.x is supported + run: pip install pytest-vcr + if: ${{ matrix.python-version != 'py39' }} - run: pytest --with-integration-tests + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/env-install + - run: pip install -r dev-requirements.txt + - run: pip install pyright + - run: pyright diff --git a/dev-requirements.txt b/dev-requirements.txt index 95bc8dbb..4bdd6b17 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -112,7 +112,7 @@ pyproject-hooks==1.2.0 # via # build # pip-tools -pytest==8.4.0 +pytest==8.4.1 # via elastic-opentelemetry (pyproject.toml) requests==2.32.4 # via @@ -132,8 +132,10 @@ typing-extensions==4.14.0 # opentelemetry-resourcedetector-gcp # opentelemetry-sdk # opentelemetry-semantic-conventions -urllib3==2.4.0 +urllib3==2.5.0 # via requests +uuid-utils==0.11.0 + # via elastic-opentelemetry (pyproject.toml) wheel==0.45.1 # via pip-tools wrapt==1.17.2 diff --git a/opamp-gen-requirements.txt b/opamp-gen-requirements.txt new file mode 100644 index 00000000..3cd7e79a --- /dev/null +++ b/opamp-gen-requirements.txt @@ -0,0 +1,5 @@ +# Use caution when bumping this version to ensure compatibility with the currently supported protobuf version. +# Pinning this to the oldest grpcio version that supports protobuf 5 helps avoid RuntimeWarning messages +# from the generated protobuf code and ensures continued stability for newer grpcio versions. +grpcio-tools==1.63.2 +mypy-protobuf~=3.5.0 diff --git a/pyproject.toml b/pyproject.toml index 16d9ce31..7f814403 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,10 +38,11 @@ dependencies = [ "opentelemetry-sdk-extension-aws ~= 2.1.0", "opentelemetry-semantic-conventions == 0.55b1", "packaging", + "uuid-utils", ] [project.optional-dependencies] -dev = ["pytest", "pip-tools", "oteltest==0.24.0", "leb128"] +dev = ["pytest", "pip-tools", "oteltest==0.24.0", "leb128", "pytest-vcr ; python_version > '3.9'"] [project.entry-points.opentelemetry_configurator] configurator = "elasticotel.distro:ElasticOpenTelemetryConfigurator" @@ -86,9 +87,26 @@ build-backend = "setuptools.build_meta" [tool.ruff] target-version = "py38" line-length = 120 +extend-exclude = [ + "*_pb2*.py*", +] [tool.ruff.lint.isort] known-third-party = [ "opentelemetry", ] known-first-party = ["elasticotel"] + +[tool.pyright] +typeCheckingMode = "standard" +pythonVersion = "3.9" + +include = [ + "src/elasticotel", + "src/opentelemetry", +] + +exclude = [ + "**/__pycache__", + "src/opentelemetry/_opamp/proto", +] diff --git a/scripts/license_headers_check.sh b/scripts/license_headers_check.sh index e5480114..5a35bab3 100755 --- a/scripts/license_headers_check.sh +++ b/scripts/license_headers_check.sh @@ -17,14 +17,15 @@ if [ $# -eq 0 ] then - FILES=$(find . \( -name "*.py" -o -name "*.c" -o -name "*.sh" \) -size +1c -not -path "./dist/*" -not -path "./build/*" -not -path "./venv*/*") + FILES=$(git ls-files | grep -e "\.py$" -e "\.c$" -e "\.sh$" | grep -v -e "/proto/" | xargs -r -d'\n' -I{} find {} -size +1c) else FILES=$@ fi LICENSE_HEADER="Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one" +UPSTREAM_LICENSE_HEADER="Copyright The OpenTelemetry Authors" -MISSING=$(grep --files-without-match "$LICENSE_HEADER" ${FILES}) +MISSING=$(grep --files-without-match -e "$LICENSE_HEADER" -e "$UPSTREAM_LICENSE_HEADER" ${FILES}) if [ -z "$MISSING" ] then diff --git a/scripts/opamp_proto_codegen.sh b/scripts/opamp_proto_codegen.sh new file mode 100755 index 00000000..f6754876 --- /dev/null +++ b/scripts/opamp_proto_codegen.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Regenerate python code from opamp protos in +# https://github.com/open-telemetry/opamp-spec +# +# To use, update OPAMP_SPEC_REPO_BRANCH_OR_COMMIT variable below to a commit hash or +# tag in opentelemtry-proto repo that you want to build off of. Then, just run +# this script to update the proto files. Commit the changes as well as any +# fixes needed in the OTLP exporter. +# +# Optional envars: +# OPAMP_SPEC_REPO_DIR - the path to an existing checkout of the opamp-spec repo + +# Pinned commit/branch/tag for the current version used in the opamp python package. +OPAMP_SPEC_REPO_BRANCH_OR_COMMIT="v0.12.0" + +set -e + +OPAMP_SPEC_REPO_DIR=${OPAMP_SPEC_REPO_DIR:-"/tmp/opamp-spec"} +# root of opentelemetry-python repo +repo_root="$(git rev-parse --show-toplevel)" +venv_dir="/tmp/opamp_proto_codegen_venv" +proto_output_dir="$repo_root/src/opentelemetry/_opamp/proto" + +# run on exit even if crash +cleanup() { + echo "Deleting $venv_dir" + rm -rf $venv_dir +} +trap cleanup EXIT + +echo "Creating temporary virtualenv at $venv_dir using $(python3 --version)" +python3 -m venv $venv_dir +source $venv_dir/bin/activate +python -m pip install \ + -c $repo_root/opamp-gen-requirements.txt \ + grpcio-tools mypy-protobuf +echo 'python -m grpc_tools.protoc --version' +python -m grpc_tools.protoc --version + +# Clone the proto repo if it doesn't exist +if [ ! -d "$OPAMP_SPEC_REPO_DIR" ]; then + git clone https://github.com/open-telemetry/opamp-spec.git $OPAMP_SPEC_REPO_DIR +fi + +# Pull in changes and switch to requested branch +( + cd $OPAMP_SPEC_REPO_DIR + git fetch --all + git checkout $OPAMP_SPEC_REPO_BRANCH_OR_COMMIT + # pull if OPAMP_SPEC_BRANCH_OR_COMMIT is not a detached head + git symbolic-ref -q HEAD && git pull --ff-only || true +) + +cd $proto_output_dir + +# clean up old generated code +find . -regex ".*_pb2.*\.pyi?" -exec rm {} + + +# generate proto code for all protos +all_protos=$(find $OPAMP_SPEC_REPO_DIR/ -name "*.proto") +python -m grpc_tools.protoc \ + -I $OPAMP_SPEC_REPO_DIR/proto \ + --python_out=. \ + --mypy_out=. \ + $all_protos + +sed -i -e 's/import anyvalue_pb2 as anyvalue__pb2/from . import anyvalue_pb2 as anyvalue__pb2/' opamp_pb2.py diff --git a/src/elasticotel/distro/__init__.py b/src/elasticotel/distro/__init__.py index 1a6807de..b2372d50 100644 --- a/src/elasticotel/distro/__init__.py +++ b/src/elasticotel/distro/__init__.py @@ -16,6 +16,7 @@ import logging import os +from urllib.parse import urlparse, urlunparse from opentelemetry.environment_variables import ( OTEL_LOGS_EXPORTER, @@ -35,17 +36,56 @@ OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, OTEL_EXPORTER_OTLP_PROTOCOL, ) +from opentelemetry.sdk.resources import OTELResourceDetector from opentelemetry.util._importlib_metadata import EntryPoint +from opentelemetry._opamp.agent import OpAMPAgent +from opentelemetry._opamp.client import OpAMPClient +from opentelemetry._opamp.proto import opamp_pb2 as opamp_pb2 -from elasticotel.distro.environment_variables import ELASTIC_OTEL_SYSTEM_METRICS_ENABLED +from elasticotel.distro.environment_variables import ELASTIC_OTEL_OPAMP_ENDPOINT, ELASTIC_OTEL_SYSTEM_METRICS_ENABLED from elasticotel.distro.resource_detectors import get_cloud_resource_detectors +from elasticotel.distro.config import opamp_handler logger = logging.getLogger(__name__) class ElasticOpenTelemetryConfigurator(_OTelSDKConfigurator): - pass + def _configure(self, **kwargs): + super()._configure(**kwargs) + + enable_opamp = False + endpoint = os.environ.get(ELASTIC_OTEL_OPAMP_ENDPOINT) + if endpoint: + parsed = urlparse(endpoint) + enable_opamp = parsed.scheme in ("http", "https") and parsed.netloc + if enable_opamp: + if not parsed.path: + parsed = parsed._replace(path="/v1/opamp") + + endpoint_url = urlunparse(parsed) + # this is not great but we don't have the calculated resource attributes around + resource = OTELResourceDetector().detect() + agent_identifying_attributes = { + "service.name": resource.attributes.get("service.name"), + } + if deployment_environment_name := resource.attributes.get( + "deployment.environment.name", resource.attributes.get("deployment.environment") + ): + agent_identifying_attributes["deployment.environment.name"] = deployment_environment_name + + opamp_client = OpAMPClient( + endpoint=endpoint_url, + agent_identifying_attributes=agent_identifying_attributes, + ) + opamp_agent = OpAMPAgent( + interval=30, + message_handler=opamp_handler, + client=opamp_client, + ) + opamp_agent.start() + else: + logger.warning("Found invalid value for OpAMP endpoint") class ElasticOpenTelemetryDistro(BaseDistro): @@ -63,7 +103,7 @@ def load_instrumentor(self, entry_point: EntryPoint, **kwargs): instrumentor_kwargs["config"] = { k: v for k, v in SYSTEM_METRICS_DEFAULT_CONFIG.items() if k.startswith("process.runtime") } - instrumentor_class(**instrumentor_kwargs).instrument(**kwargs) + instrumentor_class(**instrumentor_kwargs).instrument(**kwargs) # type: ignore[reportCallIssue] def _configure(self, **kwargs): os.environ.setdefault(OTEL_TRACES_EXPORTER, "otlp") diff --git a/src/elasticotel/distro/config.py b/src/elasticotel/distro/config.py new file mode 100644 index 00000000..3ced6db8 --- /dev/null +++ b/src/elasticotel/distro/config.py @@ -0,0 +1,57 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +from opentelemetry._opamp import messages +from opentelemetry._opamp.client import OpAMPClient +from opentelemetry._opamp.proto import opamp_pb2 as opamp_pb2 + + +logger = logging.getLogger(__name__) + +_LOG_LEVELS_MAP = { + "trace": 5, + "debug": logging.DEBUG, + "info": logging.INFO, + "warn": logging.WARNING, + "error": logging.ERROR, + "fatal": logging.CRITICAL, + "off": 1000, +} + + +def opamp_handler(client: OpAMPClient, message: opamp_pb2.ServerToAgent): + if not message.remote_config: + return + + for config_filename, config in messages._decode_remote_config(message.remote_config): + # we don't have standardized config values so limit to configs coming from our backend + if config_filename == "elastic": + logger.debug("Config %s: %s", config_filename, config) + # when config option has default value you don't get it so need to handle the default + config_logging_level = config.get("logging_level") + if config_logging_level is not None: + logging_level = _LOG_LEVELS_MAP.get(config_logging_level) # type: ignore[reportArgumentType] + else: + logging_level = logging.INFO + + if logging_level is None: + logger.warning("Logging level not handled: %s", config_logging_level) + else: + # update upstream and distro logging levels + logging.getLogger("opentelemetry").setLevel(logging_level) + logging.getLogger("elasticotel").setLevel(logging_level) diff --git a/src/elasticotel/distro/environment_variables.py b/src/elasticotel/distro/environment_variables.py index 57b285aa..7736019c 100644 --- a/src/elasticotel/distro/environment_variables.py +++ b/src/elasticotel/distro/environment_variables.py @@ -22,3 +22,12 @@ **Default value:** ``false`` """ + +ELASTIC_OTEL_OPAMP_ENDPOINT = "ELASTIC_OTEL_OPAMP_ENDPOINT" +""" +.. envvar:: ELASTIC_OTEL_OPAMP_ENDPOINT + +OpAMP Endpoint URL. + +**Default value:** ``not set`` +""" diff --git a/src/opentelemetry/_opamp/README.md b/src/opentelemetry/_opamp/README.md new file mode 100644 index 00000000..fd60725a --- /dev/null +++ b/src/opentelemetry/_opamp/README.md @@ -0,0 +1,7 @@ +# opamp + +opamp is an OpAMP protocol implementation. + +Implementation tries to be agnostic to the transport libraries and protocols used but since it's only HTTP for now that +may be achieved once more transport implementation appears. + diff --git a/src/opentelemetry/_opamp/__init__.py b/src/opentelemetry/_opamp/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/opentelemetry/_opamp/__init__.py @@ -0,0 +1 @@ + diff --git a/src/opentelemetry/_opamp/agent.py b/src/opentelemetry/_opamp/agent.py new file mode 100644 index 00000000..eaa62cde --- /dev/null +++ b/src/opentelemetry/_opamp/agent.py @@ -0,0 +1,231 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import atexit +import logging +import queue +import random +import threading +from typing import Any, Callable + +from opentelemetry._opamp.client import OpAMPClient +from opentelemetry._opamp.proto import opamp_pb2 as opamp_pb2 + +logger = logging.getLogger(__name__) + + +class _Job: + """ + Represents a single request job, with retry/backoff metadata. + """ + + def __init__( + self, + payload: Any, + max_retries: int = 1, + initial_backoff: float = 1.0, + callback: Callable[..., None] | None = None, + ): + self.payload = payload + self.attempt = 0 + self.max_retries = max_retries + self.initial_backoff = initial_backoff + # callback is called after OpAMP message handler is executed + self.callback = callback + + def should_retry(self) -> bool: + """Checks if we should retry again""" + return self.attempt <= self.max_retries + + def delay(self) -> float: + """Calculate the delay before next retry""" + assert self.attempt > 0 + return self.initial_backoff * (2 ** (self.attempt - 1)) * random.uniform(0.8, 1.2) + + +class OpAMPAgent: + """ + OpAMPAgent handles: + - periodic “heartbeat” calls enqueued at a fixed interval + - on-demand calls via send() + - exponential backoff retry on failures + - immediate cancellation of all jobs on shutdown + """ + + def __init__( + self, + *, + interval: float, + message_handler: Callable[[OpAMPClient, opamp_pb2.ServerToAgent], None], + max_retries: int = 10, + heartbeat_max_retries: int = 1, + initial_backoff: float = 1.0, + client: OpAMPClient, + ): + """ + :param interval: seconds between automatic calls + :param message_handler: user provided function that takes the received ServerToAgent message + :param max_retries: how many times to retry a failed job for ad-hoc messages + :param heartbeat_max_retries: how many times to retry an heartbeat failed job + :param initial_backoff: base seconds for exponential backoff + :param client: an OpAMPClient instance + """ + self._interval = interval + self._handler = message_handler + self._max_retries = max_retries + self._heartbeat_max_retries = heartbeat_max_retries + self._initial_backoff = initial_backoff + + self._queue: queue.Queue[_Job] = queue.Queue() + self._stop = threading.Event() + + self._worker = threading.Thread(name="OpAMPAgentWorker", target=self._run_worker, daemon=True) + self._scheduler = threading.Thread(name="OpAMPAgentScheduler", target=self._run_scheduler, daemon=True) + # start scheduling only after connection with server has been established + self._schedule = False + + self._client = client + + def _enable_scheduler(self): + self._schedule = True + logger.debug("Connected with endpoint, enabling heartbeat") + + def start(self) -> None: + """ + Starts the scheduler and worker threads. + """ + self._stop.clear() + self._worker.start() + self._scheduler.start() + + atexit.register(self.stop) + + # enqueue the connection message so we can then enable heartbeat + payload = self._client._build_connection_message() + self.send(payload, max_retries=self._max_retries, callback=self._enable_scheduler) + + def send(self, payload: Any, max_retries: int | None = None, callback: Callable[..., None] | None = None) -> None: + """ + Enqueue an on-demand request. + """ + if not self._worker.is_alive(): + logger.warning("Called send() but worker thread is not alive. Worker threads is started with start()") + + if max_retries is None: + max_retries = self._max_retries + job = _Job(payload, max_retries=max_retries, initial_backoff=self._initial_backoff, callback=callback) + self._queue.put(job) + logger.debug("On-demand job enqueued: %r", payload) + + def _run_scheduler(self) -> None: + """ + After me made a connection periodically enqueue “heartbeat” jobs until stop is signaled. + """ + while not self._stop.wait(self._interval): + if self._schedule: + payload = self._client._build_heartbeat_message() + job = _Job( + payload=payload, max_retries=self._heartbeat_max_retries, initial_backoff=self._initial_backoff + ) + self._queue.put(job) + logger.debug("Periodic job enqueued") + + def _run_worker(self) -> None: + """ + Worker loop: pull jobs, attempt the message handler, retry on failure with backoff. + """ + while not self._stop.is_set(): + try: + job: _Job = self._queue.get(timeout=1) + except queue.Empty: + continue + + message = None + while job.should_retry() and not self._stop.is_set(): + try: + message = self._client._send(job.payload) + logger.info("Job succeeded: %r", job.payload) + break + except Exception as exc: + job.attempt += 1 + logger.warning("Job %r failed attempt %d/%d: %s", job.payload, job.attempt, job.max_retries, exc) + + if not job.should_retry(): + logger.error("Job %r dropped after max retries", job.payload) + logger.exception(exc) + break + + # exponential backoff with +/- 20% jitter, interruptible by stop event + delay = job.delay() + logger.debug("Retrying in %.1fs", delay) + if self._stop.wait(delay): + # stop requested during backoff: abandon job + logger.debug("Stop signaled, abandoning job %r", job.payload) + break + + # we can't do much if the handler fails other than logging + if message is not None: + try: + self._handler(self._client, message) + logger.debug("Called Job message handler for: %r", message) + except Exception as exc: + logger.warning("Job %r handler failed with: %s", job.payload, exc) + + try: + if job.callback is not None: + job.callback() + except Exception as exc: + logging.warning("Callback for job failed: %s", exc) + finally: + self._queue.task_done() + + def stop(self) -> None: + """ + Immediately cancel all in-flight and queued jobs, then join threads. + """ + + # Before exiting send signal the server we are disconnecting to free our resources + # This is not required by the spec but is helpful in practice + logger.debug("Stopping OpAMPClient: sending AgentDisconnect") + payload = self._client._build_agent_disconnect_message() + try: + self._client._send(payload) + except Exception: + logger.debug("Stopping OpAMPClient: failed to send AgentDisconnect message") + + logger.debug("Stopping OpAMPClient: cancelling jobs") + # Clear pending jobs + while True: + try: + self._queue.get_nowait() + self._queue.task_done() + except queue.Empty: + break + + # Signal threads to exit + self._stop.set() + # don't crash if the user calls stop() before start() or calls stop() multiple times + try: + self._worker.join() + except RuntimeError as exc: + logger.warning("Stopping OpAMPClient: worker thread failed to join %s", exc) + try: + self._scheduler.join() + except RuntimeError as exc: + logger.warning("Stopping OpAMPClient: scheduler thread failed to join %s", exc) + logger.debug("OpAMPClient stopped") diff --git a/src/opentelemetry/_opamp/client.py b/src/opentelemetry/_opamp/client.py new file mode 100644 index 00000000..46005ca6 --- /dev/null +++ b/src/opentelemetry/_opamp/client.py @@ -0,0 +1,112 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import getLogger +from typing import Generator, Mapping + +from uuid_utils import uuid7 +from opentelemetry.util.types import AnyValue + +import opentelemetry._opamp.messages as messages +from opentelemetry._opamp.transport.requests import RequestsTransport +from opentelemetry._opamp.version import __version__ +from opentelemetry._opamp.proto import opamp_pb2 + + +_logger = getLogger(__name__) + +_DEFAULT_OPAMP_TIMEOUT_MS = 1_000 + +_OTLP_HTTP_HEADERS = { + "Content-Type": "application/x-protobuf", + "User-Agent": "OTel-OpAMP-Python/" + __version__, +} + +_HANDLED_CAPABILITIES = ( + opamp_pb2.AgentCapabilities.AgentCapabilities_ReportsStatus + | opamp_pb2.AgentCapabilities.AgentCapabilities_ReportsHeartbeat + | opamp_pb2.AgentCapabilities.AgentCapabilities_AcceptsRemoteConfig +) + + +class OpAMPClient: + def __init__( + self, + *, + endpoint: str, + headers: Mapping[str, str] | None = None, + timeout_millis: int = _DEFAULT_OPAMP_TIMEOUT_MS, + agent_identifying_attributes: Mapping[str, AnyValue], + agent_non_identifying_attributes: Mapping[str, AnyValue] | None = None, + ): + self._timeout_millis = timeout_millis + self._transport = RequestsTransport() + + self._endpoint = endpoint + headers = headers or {} + self._headers = {**_OTLP_HTTP_HEADERS, **headers} + + self._agent_description = messages._build_agent_description( + identifying_attributes=agent_identifying_attributes, + non_identifying_attributes=agent_non_identifying_attributes, + ) + self._sequence_num: int = 0 + self._instance_uid: bytes = uuid7().bytes + + def _build_connection_message(self) -> bytes: + message = messages._build_presentation_message( + instance_uid=self._instance_uid, + agent_description=self._agent_description, + sequence_num=self._sequence_num, + capabilities=_HANDLED_CAPABILITIES, + ) + data = messages._encode_message(message) + return data + + def _build_agent_disconnect_message(self) -> bytes: + message = messages._build_agent_disconnect_message( + instance_uid=self._instance_uid, + sequence_num=self._sequence_num, + capabilities=_HANDLED_CAPABILITIES, + ) + data = messages._encode_message(message) + return data + + def _build_heartbeat_message(self) -> bytes: + message = messages._build_heartbeat_message( + instance_uid=self._instance_uid, sequence_num=self._sequence_num, capabilities=_HANDLED_CAPABILITIES + ) + data = messages._encode_message(message) + return data + + def _send(self, data: bytes): + try: + response = self._transport.send( + url=self._endpoint, headers=self._headers, data=data, timeout_millis=self._timeout_millis + ) + return response + except: + raise + finally: + self._sequence_num += 1 + + def _decode_remote_config( + self, remote_config: opamp_pb2.AgentRemoteConfig + ) -> Generator[tuple[str, Mapping[str, AnyValue]]]: + for config_file, config in messages._decode_remote_config(remote_config): + yield config_file, config diff --git a/src/opentelemetry/_opamp/exceptions.py b/src/opentelemetry/_opamp/exceptions.py new file mode 100644 index 00000000..bec94334 --- /dev/null +++ b/src/opentelemetry/_opamp/exceptions.py @@ -0,0 +1,27 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class OpAMPTimeoutError(Exception): + pass + + +class OpAMPRemoteConfigParseException(Exception): + pass + + +class OpAMPRemoteConfigDecodeException(Exception): + pass diff --git a/src/opentelemetry/_opamp/messages.py b/src/opentelemetry/_opamp/messages.py new file mode 100644 index 00000000..b3caa6f7 --- /dev/null +++ b/src/opentelemetry/_opamp/messages.py @@ -0,0 +1,116 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import json +from typing import Generator, Mapping + +from opentelemetry.util.types import AnyValue + +from opentelemetry._opamp.proto import opamp_pb2 as opamp_pb2 +from opentelemetry._opamp.proto.anyvalue_pb2 import KeyValue as PB2KeyValue, AnyValue as PB2AnyValue +from opentelemetry._opamp.exceptions import OpAMPRemoteConfigParseException, OpAMPRemoteConfigDecodeException + + +def _decode_message(data: bytes) -> opamp_pb2.ServerToAgent: + message = opamp_pb2.ServerToAgent() + message.ParseFromString(data) + return message + + +def _encode_value(value: AnyValue) -> PB2AnyValue: + if value is None: + return PB2AnyValue() + if isinstance(value, bool): + return PB2AnyValue(bool_value=value) + if isinstance(value, int): + return PB2AnyValue(int_value=value) + if isinstance(value, float): + return PB2AnyValue(double_value=value) + if isinstance(value, str): + return PB2AnyValue(string_value=value) + if isinstance(value, bytes): + return PB2AnyValue(bytes_value=value) + # TODO: handle sequence and mapping? + raise ValueError(f"Invalid type {type(value)} of value {value}") + + +def _encode_attributes(attributes: Mapping[str, AnyValue]): + return [PB2KeyValue(key=key, value=_encode_value(value)) for key, value in attributes.items()] + + +def _build_agent_description( + identifying_attributes: Mapping[str, AnyValue], + non_identifying_attributes: Mapping[str, AnyValue] | None = None, +) -> opamp_pb2.AgentDescription: + identifying_attrs = _encode_attributes(identifying_attributes) + non_identifying_attrs = _encode_attributes(non_identifying_attributes) if non_identifying_attributes else None + return opamp_pb2.AgentDescription( + identifying_attributes=identifying_attrs, non_identifying_attributes=non_identifying_attrs + ) + + +def _build_presentation_message( + instance_uid: bytes, sequence_num: int, agent_description: opamp_pb2.AgentDescription, capabilities: int +) -> opamp_pb2.AgentToServer: + command = opamp_pb2.AgentToServer( + instance_uid=instance_uid, + sequence_num=sequence_num, + agent_description=agent_description, + capabilities=capabilities, + ) + return command + + +def _build_heartbeat_message(instance_uid: bytes, sequence_num: int, capabilities: int) -> opamp_pb2.AgentToServer: + command = opamp_pb2.AgentToServer(instance_uid=instance_uid, sequence_num=sequence_num, capabilities=capabilities) + return command + + +def _build_agent_disconnect_message( + instance_uid: bytes, sequence_num: int, capabilities: int +) -> opamp_pb2.AgentToServer: + command = opamp_pb2.AgentToServer( + instance_uid=instance_uid, + sequence_num=sequence_num, + agent_disconnect=opamp_pb2.AgentDisconnect(), + capabilities=capabilities, + ) + return command + + +def _encode_message(data: opamp_pb2.AgentToServer) -> bytes: + return data.SerializeToString() + + +def _decode_remote_config(remote_config: opamp_pb2.AgentRemoteConfig) -> Generator[tuple[str, Mapping[str, AnyValue]]]: + for config_file_name, config_file in remote_config.config.config_map.items(): + if config_file.content_type in ("application/json", "text/json"): + try: + body = config_file.body.decode() + config_data = json.loads(body) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise OpAMPRemoteConfigDecodeException( + f"Failed to decode {config_file} with content type {config_file.content_type}: {exc}" + ) + continue + + yield config_file_name, config_data + else: + raise OpAMPRemoteConfigParseException( + f"Cannot parse {config_file_name} with content type {config_file.content_type}" + ) diff --git a/src/opentelemetry/_opamp/proto/anyvalue_pb2.py b/src/opentelemetry/_opamp/proto/anyvalue_pb2.py new file mode 100644 index 00000000..7d1cd9b5 --- /dev/null +++ b/src/opentelemetry/_opamp/proto/anyvalue_pb2.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: anyvalue.proto +# Protobuf Python Version: 5.26.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0e\x61nyvalue.proto\x12\x0bopamp.proto\"\xe8\x01\n\x08\x41nyValue\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12.\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32\x17.opamp.proto.ArrayValueH\x00\x12\x31\n\x0ckvlist_value\x18\x06 \x01(\x0b\x32\x19.opamp.proto.KeyValueListH\x00\x12\x15\n\x0b\x62ytes_value\x18\x07 \x01(\x0cH\x00\x42\x07\n\x05value\"3\n\nArrayValue\x12%\n\x06values\x18\x01 \x03(\x0b\x32\x15.opamp.proto.AnyValue\"5\n\x0cKeyValueList\x12%\n\x06values\x18\x01 \x03(\x0b\x32\x15.opamp.proto.KeyValue\"=\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12$\n\x05value\x18\x02 \x01(\x0b\x32\x15.opamp.proto.AnyValueB.Z,github.com/open-telemetry/opamp-go/protobufsb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'anyvalue_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z,github.com/open-telemetry/opamp-go/protobufs' + _globals['_ANYVALUE']._serialized_start=32 + _globals['_ANYVALUE']._serialized_end=264 + _globals['_ARRAYVALUE']._serialized_start=266 + _globals['_ARRAYVALUE']._serialized_end=317 + _globals['_KEYVALUELIST']._serialized_start=319 + _globals['_KEYVALUELIST']._serialized_end=372 + _globals['_KEYVALUE']._serialized_start=374 + _globals['_KEYVALUE']._serialized_end=435 +# @@protoc_insertion_point(module_scope) diff --git a/src/opentelemetry/_opamp/proto/anyvalue_pb2.pyi b/src/opentelemetry/_opamp/proto/anyvalue_pb2.pyi new file mode 100644 index 00000000..5036b8eb --- /dev/null +++ b/src/opentelemetry/_opamp/proto/anyvalue_pb2.pyi @@ -0,0 +1,135 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +This file is copied and modified from https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/common/v1/common.proto +Modifications: + - Removal of unneeded InstrumentationLibrary and StringKeyValue messages. + - Change of go_package to reference a package in this repo. + - Removal of gogoproto usage. +""" +import builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +@typing_extensions.final +class AnyValue(google.protobuf.message.Message): + """AnyValue is used to represent any type of attribute value. AnyValue may contain a + primitive value such as a string or integer or it may contain an arbitrary nested + object containing arrays, key-value lists and primitives. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + STRING_VALUE_FIELD_NUMBER: builtins.int + BOOL_VALUE_FIELD_NUMBER: builtins.int + INT_VALUE_FIELD_NUMBER: builtins.int + DOUBLE_VALUE_FIELD_NUMBER: builtins.int + ARRAY_VALUE_FIELD_NUMBER: builtins.int + KVLIST_VALUE_FIELD_NUMBER: builtins.int + BYTES_VALUE_FIELD_NUMBER: builtins.int + string_value: builtins.str + bool_value: builtins.bool + int_value: builtins.int + double_value: builtins.float + @property + def array_value(self) -> global___ArrayValue: ... + @property + def kvlist_value(self) -> global___KeyValueList: ... + bytes_value: builtins.bytes + def __init__( + self, + *, + string_value: builtins.str = ..., + bool_value: builtins.bool = ..., + int_value: builtins.int = ..., + double_value: builtins.float = ..., + array_value: global___ArrayValue | None = ..., + kvlist_value: global___KeyValueList | None = ..., + bytes_value: builtins.bytes = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["array_value", b"array_value", "bool_value", b"bool_value", "bytes_value", b"bytes_value", "double_value", b"double_value", "int_value", b"int_value", "kvlist_value", b"kvlist_value", "string_value", b"string_value", "value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["array_value", b"array_value", "bool_value", b"bool_value", "bytes_value", b"bytes_value", "double_value", b"double_value", "int_value", b"int_value", "kvlist_value", b"kvlist_value", "string_value", b"string_value", "value", b"value"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["value", b"value"]) -> typing_extensions.Literal["string_value", "bool_value", "int_value", "double_value", "array_value", "kvlist_value", "bytes_value"] | None: ... + +global___AnyValue = AnyValue + +@typing_extensions.final +class ArrayValue(google.protobuf.message.Message): + """ArrayValue is a list of AnyValue messages. We need ArrayValue as a message + since oneof in AnyValue does not allow repeated fields. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VALUES_FIELD_NUMBER: builtins.int + @property + def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___AnyValue]: + """Array of values. The array may be empty (contain 0 elements).""" + def __init__( + self, + *, + values: collections.abc.Iterable[global___AnyValue] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["values", b"values"]) -> None: ... + +global___ArrayValue = ArrayValue + +@typing_extensions.final +class KeyValueList(google.protobuf.message.Message): + """KeyValueList is a list of KeyValue messages. We need KeyValueList as a message + since `oneof` in AnyValue does not allow repeated fields. Everywhere else where we need + a list of KeyValue messages (e.g. in Span) we use `repeated KeyValue` directly to + avoid unnecessary extra wrapping (which slows down the protocol). The 2 approaches + are semantically equivalent. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + VALUES_FIELD_NUMBER: builtins.int + @property + def values(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___KeyValue]: + """A collection of key/value pairs of key-value pairs. The list may be empty (may + contain 0 elements). + """ + def __init__( + self, + *, + values: collections.abc.Iterable[global___KeyValue] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["values", b"values"]) -> None: ... + +global___KeyValueList = KeyValueList + +@typing_extensions.final +class KeyValue(google.protobuf.message.Message): + """KeyValue is a key-value pair that is used to store Span attributes, Link + attributes, etc. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___AnyValue: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___AnyValue | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + +global___KeyValue = KeyValue diff --git a/src/opentelemetry/_opamp/proto/opamp_pb2.py b/src/opentelemetry/_opamp/proto/opamp_pb2.py new file mode 100644 index 00000000..00a62682 --- /dev/null +++ b/src/opentelemetry/_opamp/proto/opamp_pb2.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opamp.proto +# Protobuf Python Version: 5.26.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from . import anyvalue_pb2 as anyvalue__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bopamp.proto\x12\x0bopamp.proto\x1a\x0e\x61nyvalue.proto\"\xae\x05\n\rAgentToServer\x12\x14\n\x0cinstance_uid\x18\x01 \x01(\x0c\x12\x14\n\x0csequence_num\x18\x02 \x01(\x04\x12\x38\n\x11\x61gent_description\x18\x03 \x01(\x0b\x32\x1d.opamp.proto.AgentDescription\x12\x14\n\x0c\x63\x61pabilities\x18\x04 \x01(\x04\x12,\n\x06health\x18\x05 \x01(\x0b\x32\x1c.opamp.proto.ComponentHealth\x12\x36\n\x10\x65\x66\x66\x65\x63tive_config\x18\x06 \x01(\x0b\x32\x1c.opamp.proto.EffectiveConfig\x12=\n\x14remote_config_status\x18\x07 \x01(\x0b\x32\x1f.opamp.proto.RemoteConfigStatus\x12\x36\n\x10package_statuses\x18\x08 \x01(\x0b\x32\x1c.opamp.proto.PackageStatuses\x12\x36\n\x10\x61gent_disconnect\x18\t \x01(\x0b\x32\x1c.opamp.proto.AgentDisconnect\x12\r\n\x05\x66lags\x18\n \x01(\x04\x12K\n\x1b\x63onnection_settings_request\x18\x0b \x01(\x0b\x32&.opamp.proto.ConnectionSettingsRequest\x12<\n\x13\x63ustom_capabilities\x18\x0c \x01(\x0b\x32\x1f.opamp.proto.CustomCapabilities\x12\x32\n\x0e\x63ustom_message\x18\r \x01(\x0b\x32\x1a.opamp.proto.CustomMessage\x12>\n\x14\x61vailable_components\x18\x0e \x01(\x0b\x32 .opamp.proto.AvailableComponents\"\x11\n\x0f\x41gentDisconnect\"W\n\x19\x43onnectionSettingsRequest\x12:\n\x05opamp\x18\x01 \x01(\x0b\x32+.opamp.proto.OpAMPConnectionSettingsRequest\"^\n\x1eOpAMPConnectionSettingsRequest\x12<\n\x13\x63\x65rtificate_request\x18\x01 \x01(\x0b\x32\x1f.opamp.proto.CertificateRequest\"!\n\x12\x43\x65rtificateRequest\x12\x0b\n\x03\x63sr\x18\x01 \x01(\x0c\"\xbb\x01\n\x13\x41vailableComponents\x12\x44\n\ncomponents\x18\x01 \x03(\x0b\x32\x30.opamp.proto.AvailableComponents.ComponentsEntry\x12\x0c\n\x04hash\x18\x02 \x01(\x0c\x1aP\n\x0f\x43omponentsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12,\n\x05value\x18\x02 \x01(\x0b\x32\x1d.opamp.proto.ComponentDetails:\x02\x38\x01\"\xe1\x01\n\x10\x43omponentDetails\x12\'\n\x08metadata\x18\x01 \x03(\x0b\x32\x15.opamp.proto.KeyValue\x12M\n\x11sub_component_map\x18\x02 \x03(\x0b\x32\x32.opamp.proto.ComponentDetails.SubComponentMapEntry\x1aU\n\x14SubComponentMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12,\n\x05value\x18\x02 \x01(\x0b\x32\x1d.opamp.proto.ComponentDetails:\x02\x38\x01\"\xa1\x04\n\rServerToAgent\x12\x14\n\x0cinstance_uid\x18\x01 \x01(\x0c\x12\x38\n\x0e\x65rror_response\x18\x02 \x01(\x0b\x32 .opamp.proto.ServerErrorResponse\x12\x35\n\rremote_config\x18\x03 \x01(\x0b\x32\x1e.opamp.proto.AgentRemoteConfig\x12\x42\n\x13\x63onnection_settings\x18\x04 \x01(\x0b\x32%.opamp.proto.ConnectionSettingsOffers\x12:\n\x12packages_available\x18\x05 \x01(\x0b\x32\x1e.opamp.proto.PackagesAvailable\x12\r\n\x05\x66lags\x18\x06 \x01(\x04\x12\x14\n\x0c\x63\x61pabilities\x18\x07 \x01(\x04\x12>\n\x14\x61gent_identification\x18\x08 \x01(\x0b\x32 .opamp.proto.AgentIdentification\x12\x32\n\x07\x63ommand\x18\t \x01(\x0b\x32!.opamp.proto.ServerToAgentCommand\x12<\n\x13\x63ustom_capabilities\x18\n \x01(\x0b\x32\x1f.opamp.proto.CustomCapabilities\x12\x32\n\x0e\x63ustom_message\x18\x0b \x01(\x0b\x32\x1a.opamp.proto.CustomMessage\"\xb4\x01\n\x17OpAMPConnectionSettings\x12\x1c\n\x14\x64\x65stination_endpoint\x18\x01 \x01(\t\x12%\n\x07headers\x18\x02 \x01(\x0b\x32\x14.opamp.proto.Headers\x12\x30\n\x0b\x63\x65rtificate\x18\x03 \x01(\x0b\x32\x1b.opamp.proto.TLSCertificate\x12\"\n\x1aheartbeat_interval_seconds\x18\x04 \x01(\x04\"\x94\x01\n\x1bTelemetryConnectionSettings\x12\x1c\n\x14\x64\x65stination_endpoint\x18\x01 \x01(\t\x12%\n\x07headers\x18\x02 \x01(\x0b\x32\x14.opamp.proto.Headers\x12\x30\n\x0b\x63\x65rtificate\x18\x03 \x01(\x0b\x32\x1b.opamp.proto.TLSCertificate\"\x97\x02\n\x17OtherConnectionSettings\x12\x1c\n\x14\x64\x65stination_endpoint\x18\x01 \x01(\t\x12%\n\x07headers\x18\x02 \x01(\x0b\x32\x14.opamp.proto.Headers\x12\x30\n\x0b\x63\x65rtificate\x18\x03 \x01(\x0b\x32\x1b.opamp.proto.TLSCertificate\x12O\n\x0eother_settings\x18\x04 \x03(\x0b\x32\x37.opamp.proto.OtherConnectionSettings.OtherSettingsEntry\x1a\x34\n\x12OtherSettingsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"/\n\x07Headers\x12$\n\x07headers\x18\x01 \x03(\x0b\x32\x13.opamp.proto.Header\"$\n\x06Header\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"D\n\x0eTLSCertificate\x12\x0c\n\x04\x63\x65rt\x18\x01 \x01(\x0c\x12\x13\n\x0bprivate_key\x18\x02 \x01(\x0c\x12\x0f\n\x07\x63\x61_cert\x18\x03 \x01(\x0c\"\xcd\x03\n\x18\x43onnectionSettingsOffers\x12\x0c\n\x04hash\x18\x01 \x01(\x0c\x12\x33\n\x05opamp\x18\x02 \x01(\x0b\x32$.opamp.proto.OpAMPConnectionSettings\x12=\n\x0bown_metrics\x18\x03 \x01(\x0b\x32(.opamp.proto.TelemetryConnectionSettings\x12<\n\nown_traces\x18\x04 \x01(\x0b\x32(.opamp.proto.TelemetryConnectionSettings\x12:\n\x08own_logs\x18\x05 \x01(\x0b\x32(.opamp.proto.TelemetryConnectionSettings\x12V\n\x11other_connections\x18\x06 \x03(\x0b\x32;.opamp.proto.ConnectionSettingsOffers.OtherConnectionsEntry\x1a]\n\x15OtherConnectionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x33\n\x05value\x18\x02 \x01(\x0b\x32$.opamp.proto.OtherConnectionSettings:\x02\x38\x01\"\xbe\x01\n\x11PackagesAvailable\x12>\n\x08packages\x18\x01 \x03(\x0b\x32,.opamp.proto.PackagesAvailable.PackagesEntry\x12\x19\n\x11\x61ll_packages_hash\x18\x02 \x01(\x0c\x1aN\n\rPackagesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12,\n\x05value\x18\x02 \x01(\x0b\x32\x1d.opamp.proto.PackageAvailable:\x02\x38\x01\"\x86\x01\n\x10PackageAvailable\x12&\n\x04type\x18\x01 \x01(\x0e\x32\x18.opamp.proto.PackageType\x12\x0f\n\x07version\x18\x02 \x01(\t\x12+\n\x04\x66ile\x18\x03 \x01(\x0b\x32\x1d.opamp.proto.DownloadableFile\x12\x0c\n\x04hash\x18\x04 \x01(\x0c\"x\n\x10\x44ownloadableFile\x12\x14\n\x0c\x64ownload_url\x18\x01 \x01(\t\x12\x14\n\x0c\x63ontent_hash\x18\x02 \x01(\x0c\x12\x11\n\tsignature\x18\x03 \x01(\x0c\x12%\n\x07headers\x18\x04 \x01(\x0b\x32\x14.opamp.proto.Headers\"\x99\x01\n\x13ServerErrorResponse\x12\x32\n\x04type\x18\x01 \x01(\x0e\x32$.opamp.proto.ServerErrorResponseType\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12,\n\nretry_info\x18\x03 \x01(\x0b\x32\x16.opamp.proto.RetryInfoH\x00\x42\t\n\x07\x44\x65tails\",\n\tRetryInfo\x12\x1f\n\x17retry_after_nanoseconds\x18\x01 \x01(\x04\">\n\x14ServerToAgentCommand\x12&\n\x04type\x18\x01 \x01(\x0e\x32\x18.opamp.proto.CommandType\"\x84\x01\n\x10\x41gentDescription\x12\x35\n\x16identifying_attributes\x18\x01 \x03(\x0b\x32\x15.opamp.proto.KeyValue\x12\x39\n\x1anon_identifying_attributes\x18\x02 \x03(\x0b\x32\x15.opamp.proto.KeyValue\"\xb0\x02\n\x0f\x43omponentHealth\x12\x0f\n\x07healthy\x18\x01 \x01(\x08\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x12\n\nlast_error\x18\x03 \x01(\t\x12\x0e\n\x06status\x18\x04 \x01(\t\x12\x1d\n\x15status_time_unix_nano\x18\x05 \x01(\x06\x12R\n\x14\x63omponent_health_map\x18\x06 \x03(\x0b\x32\x34.opamp.proto.ComponentHealth.ComponentHealthMapEntry\x1aW\n\x17\x43omponentHealthMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12+\n\x05value\x18\x02 \x01(\x0b\x32\x1c.opamp.proto.ComponentHealth:\x02\x38\x01\"B\n\x0f\x45\x66\x66\x65\x63tiveConfig\x12/\n\nconfig_map\x18\x01 \x01(\x0b\x32\x1b.opamp.proto.AgentConfigMap\"\x7f\n\x12RemoteConfigStatus\x12\x1f\n\x17last_remote_config_hash\x18\x01 \x01(\x0c\x12\x31\n\x06status\x18\x02 \x01(\x0e\x32!.opamp.proto.RemoteConfigStatuses\x12\x15\n\rerror_message\x18\x03 \x01(\t\"\xde\x01\n\x0fPackageStatuses\x12<\n\x08packages\x18\x01 \x03(\x0b\x32*.opamp.proto.PackageStatuses.PackagesEntry\x12)\n!server_provided_all_packages_hash\x18\x02 \x01(\x0c\x12\x15\n\rerror_message\x18\x03 \x01(\t\x1aK\n\rPackagesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12)\n\x05value\x18\x02 \x01(\x0b\x32\x1a.opamp.proto.PackageStatus:\x02\x38\x01\"\x93\x02\n\rPackageStatus\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x19\n\x11\x61gent_has_version\x18\x02 \x01(\t\x12\x16\n\x0e\x61gent_has_hash\x18\x03 \x01(\x0c\x12\x1e\n\x16server_offered_version\x18\x04 \x01(\t\x12\x1b\n\x13server_offered_hash\x18\x05 \x01(\x0c\x12.\n\x06status\x18\x06 \x01(\x0e\x32\x1e.opamp.proto.PackageStatusEnum\x12\x15\n\rerror_message\x18\x07 \x01(\t\x12=\n\x10\x64ownload_details\x18\x08 \x01(\x0b\x32#.opamp.proto.PackageDownloadDetails\"U\n\x16PackageDownloadDetails\x12\x18\n\x10\x64ownload_percent\x18\x01 \x01(\x01\x12!\n\x19\x64ownload_bytes_per_second\x18\x02 \x01(\x01\"/\n\x13\x41gentIdentification\x12\x18\n\x10new_instance_uid\x18\x01 \x01(\x0c\"U\n\x11\x41gentRemoteConfig\x12+\n\x06\x63onfig\x18\x01 \x01(\x0b\x32\x1b.opamp.proto.AgentConfigMap\x12\x13\n\x0b\x63onfig_hash\x18\x02 \x01(\x0c\"\xa0\x01\n\x0e\x41gentConfigMap\x12>\n\nconfig_map\x18\x01 \x03(\x0b\x32*.opamp.proto.AgentConfigMap.ConfigMapEntry\x1aN\n\x0e\x43onfigMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12+\n\x05value\x18\x02 \x01(\x0b\x32\x1c.opamp.proto.AgentConfigFile:\x02\x38\x01\"5\n\x0f\x41gentConfigFile\x12\x0c\n\x04\x62ody\x18\x01 \x01(\x0c\x12\x14\n\x0c\x63ontent_type\x18\x02 \x01(\t\"*\n\x12\x43ustomCapabilities\x12\x14\n\x0c\x63\x61pabilities\x18\x01 \x03(\t\"?\n\rCustomMessage\x12\x12\n\ncapability\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c*c\n\x12\x41gentToServerFlags\x12\"\n\x1e\x41gentToServerFlags_Unspecified\x10\x00\x12)\n%AgentToServerFlags_RequestInstanceUid\x10\x01*\x92\x01\n\x12ServerToAgentFlags\x12\"\n\x1eServerToAgentFlags_Unspecified\x10\x00\x12&\n\"ServerToAgentFlags_ReportFullState\x10\x01\x12\x30\n,ServerToAgentFlags_ReportAvailableComponents\x10\x02*\xf7\x02\n\x12ServerCapabilities\x12\"\n\x1eServerCapabilities_Unspecified\x10\x00\x12$\n ServerCapabilities_AcceptsStatus\x10\x01\x12)\n%ServerCapabilities_OffersRemoteConfig\x10\x02\x12-\n)ServerCapabilities_AcceptsEffectiveConfig\x10\x04\x12%\n!ServerCapabilities_OffersPackages\x10\x08\x12,\n(ServerCapabilities_AcceptsPackagesStatus\x10\x10\x12/\n+ServerCapabilities_OffersConnectionSettings\x10 \x12\x37\n3ServerCapabilities_AcceptsConnectionSettingsRequest\x10@*>\n\x0bPackageType\x12\x18\n\x14PackageType_TopLevel\x10\x00\x12\x15\n\x11PackageType_Addon\x10\x01*\x8f\x01\n\x17ServerErrorResponseType\x12#\n\x1fServerErrorResponseType_Unknown\x10\x00\x12&\n\"ServerErrorResponseType_BadRequest\x10\x01\x12\'\n#ServerErrorResponseType_Unavailable\x10\x02*&\n\x0b\x43ommandType\x12\x17\n\x13\x43ommandType_Restart\x10\x00*\xcc\x05\n\x11\x41gentCapabilities\x12!\n\x1d\x41gentCapabilities_Unspecified\x10\x00\x12#\n\x1f\x41gentCapabilities_ReportsStatus\x10\x01\x12)\n%AgentCapabilities_AcceptsRemoteConfig\x10\x02\x12,\n(AgentCapabilities_ReportsEffectiveConfig\x10\x04\x12%\n!AgentCapabilities_AcceptsPackages\x10\x08\x12,\n(AgentCapabilities_ReportsPackageStatuses\x10\x10\x12&\n\"AgentCapabilities_ReportsOwnTraces\x10 \x12\'\n#AgentCapabilities_ReportsOwnMetrics\x10@\x12%\n AgentCapabilities_ReportsOwnLogs\x10\x80\x01\x12\x35\n0AgentCapabilities_AcceptsOpAMPConnectionSettings\x10\x80\x02\x12\x35\n0AgentCapabilities_AcceptsOtherConnectionSettings\x10\x80\x04\x12,\n\'AgentCapabilities_AcceptsRestartCommand\x10\x80\x08\x12$\n\x1f\x41gentCapabilities_ReportsHealth\x10\x80\x10\x12*\n%AgentCapabilities_ReportsRemoteConfig\x10\x80 \x12\'\n\"AgentCapabilities_ReportsHeartbeat\x10\x80@\x12\x32\n,AgentCapabilities_ReportsAvailableComponents\x10\x80\x80\x01*\x9c\x01\n\x14RemoteConfigStatuses\x12\x1e\n\x1aRemoteConfigStatuses_UNSET\x10\x00\x12 \n\x1cRemoteConfigStatuses_APPLIED\x10\x01\x12!\n\x1dRemoteConfigStatuses_APPLYING\x10\x02\x12\x1f\n\x1bRemoteConfigStatuses_FAILED\x10\x03*\xc4\x01\n\x11PackageStatusEnum\x12\x1f\n\x1bPackageStatusEnum_Installed\x10\x00\x12$\n PackageStatusEnum_InstallPending\x10\x01\x12 \n\x1cPackageStatusEnum_Installing\x10\x02\x12#\n\x1fPackageStatusEnum_InstallFailed\x10\x03\x12!\n\x1dPackageStatusEnum_Downloading\x10\x04\x42.Z,github.com/open-telemetry/opamp-go/protobufsb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'opamp_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'Z,github.com/open-telemetry/opamp-go/protobufs' + _globals['_AVAILABLECOMPONENTS_COMPONENTSENTRY']._loaded_options = None + _globals['_AVAILABLECOMPONENTS_COMPONENTSENTRY']._serialized_options = b'8\001' + _globals['_COMPONENTDETAILS_SUBCOMPONENTMAPENTRY']._loaded_options = None + _globals['_COMPONENTDETAILS_SUBCOMPONENTMAPENTRY']._serialized_options = b'8\001' + _globals['_OTHERCONNECTIONSETTINGS_OTHERSETTINGSENTRY']._loaded_options = None + _globals['_OTHERCONNECTIONSETTINGS_OTHERSETTINGSENTRY']._serialized_options = b'8\001' + _globals['_CONNECTIONSETTINGSOFFERS_OTHERCONNECTIONSENTRY']._loaded_options = None + _globals['_CONNECTIONSETTINGSOFFERS_OTHERCONNECTIONSENTRY']._serialized_options = b'8\001' + _globals['_PACKAGESAVAILABLE_PACKAGESENTRY']._loaded_options = None + _globals['_PACKAGESAVAILABLE_PACKAGESENTRY']._serialized_options = b'8\001' + _globals['_COMPONENTHEALTH_COMPONENTHEALTHMAPENTRY']._loaded_options = None + _globals['_COMPONENTHEALTH_COMPONENTHEALTHMAPENTRY']._serialized_options = b'8\001' + _globals['_PACKAGESTATUSES_PACKAGESENTRY']._loaded_options = None + _globals['_PACKAGESTATUSES_PACKAGESENTRY']._serialized_options = b'8\001' + _globals['_AGENTCONFIGMAP_CONFIGMAPENTRY']._loaded_options = None + _globals['_AGENTCONFIGMAP_CONFIGMAPENTRY']._serialized_options = b'8\001' + _globals['_AGENTTOSERVERFLAGS']._serialized_start=5585 + _globals['_AGENTTOSERVERFLAGS']._serialized_end=5684 + _globals['_SERVERTOAGENTFLAGS']._serialized_start=5687 + _globals['_SERVERTOAGENTFLAGS']._serialized_end=5833 + _globals['_SERVERCAPABILITIES']._serialized_start=5836 + _globals['_SERVERCAPABILITIES']._serialized_end=6211 + _globals['_PACKAGETYPE']._serialized_start=6213 + _globals['_PACKAGETYPE']._serialized_end=6275 + _globals['_SERVERERRORRESPONSETYPE']._serialized_start=6278 + _globals['_SERVERERRORRESPONSETYPE']._serialized_end=6421 + _globals['_COMMANDTYPE']._serialized_start=6423 + _globals['_COMMANDTYPE']._serialized_end=6461 + _globals['_AGENTCAPABILITIES']._serialized_start=6464 + _globals['_AGENTCAPABILITIES']._serialized_end=7180 + _globals['_REMOTECONFIGSTATUSES']._serialized_start=7183 + _globals['_REMOTECONFIGSTATUSES']._serialized_end=7339 + _globals['_PACKAGESTATUSENUM']._serialized_start=7342 + _globals['_PACKAGESTATUSENUM']._serialized_end=7538 + _globals['_AGENTTOSERVER']._serialized_start=45 + _globals['_AGENTTOSERVER']._serialized_end=731 + _globals['_AGENTDISCONNECT']._serialized_start=733 + _globals['_AGENTDISCONNECT']._serialized_end=750 + _globals['_CONNECTIONSETTINGSREQUEST']._serialized_start=752 + _globals['_CONNECTIONSETTINGSREQUEST']._serialized_end=839 + _globals['_OPAMPCONNECTIONSETTINGSREQUEST']._serialized_start=841 + _globals['_OPAMPCONNECTIONSETTINGSREQUEST']._serialized_end=935 + _globals['_CERTIFICATEREQUEST']._serialized_start=937 + _globals['_CERTIFICATEREQUEST']._serialized_end=970 + _globals['_AVAILABLECOMPONENTS']._serialized_start=973 + _globals['_AVAILABLECOMPONENTS']._serialized_end=1160 + _globals['_AVAILABLECOMPONENTS_COMPONENTSENTRY']._serialized_start=1080 + _globals['_AVAILABLECOMPONENTS_COMPONENTSENTRY']._serialized_end=1160 + _globals['_COMPONENTDETAILS']._serialized_start=1163 + _globals['_COMPONENTDETAILS']._serialized_end=1388 + _globals['_COMPONENTDETAILS_SUBCOMPONENTMAPENTRY']._serialized_start=1303 + _globals['_COMPONENTDETAILS_SUBCOMPONENTMAPENTRY']._serialized_end=1388 + _globals['_SERVERTOAGENT']._serialized_start=1391 + _globals['_SERVERTOAGENT']._serialized_end=1936 + _globals['_OPAMPCONNECTIONSETTINGS']._serialized_start=1939 + _globals['_OPAMPCONNECTIONSETTINGS']._serialized_end=2119 + _globals['_TELEMETRYCONNECTIONSETTINGS']._serialized_start=2122 + _globals['_TELEMETRYCONNECTIONSETTINGS']._serialized_end=2270 + _globals['_OTHERCONNECTIONSETTINGS']._serialized_start=2273 + _globals['_OTHERCONNECTIONSETTINGS']._serialized_end=2552 + _globals['_OTHERCONNECTIONSETTINGS_OTHERSETTINGSENTRY']._serialized_start=2500 + _globals['_OTHERCONNECTIONSETTINGS_OTHERSETTINGSENTRY']._serialized_end=2552 + _globals['_HEADERS']._serialized_start=2554 + _globals['_HEADERS']._serialized_end=2601 + _globals['_HEADER']._serialized_start=2603 + _globals['_HEADER']._serialized_end=2639 + _globals['_TLSCERTIFICATE']._serialized_start=2641 + _globals['_TLSCERTIFICATE']._serialized_end=2709 + _globals['_CONNECTIONSETTINGSOFFERS']._serialized_start=2712 + _globals['_CONNECTIONSETTINGSOFFERS']._serialized_end=3173 + _globals['_CONNECTIONSETTINGSOFFERS_OTHERCONNECTIONSENTRY']._serialized_start=3080 + _globals['_CONNECTIONSETTINGSOFFERS_OTHERCONNECTIONSENTRY']._serialized_end=3173 + _globals['_PACKAGESAVAILABLE']._serialized_start=3176 + _globals['_PACKAGESAVAILABLE']._serialized_end=3366 + _globals['_PACKAGESAVAILABLE_PACKAGESENTRY']._serialized_start=3288 + _globals['_PACKAGESAVAILABLE_PACKAGESENTRY']._serialized_end=3366 + _globals['_PACKAGEAVAILABLE']._serialized_start=3369 + _globals['_PACKAGEAVAILABLE']._serialized_end=3503 + _globals['_DOWNLOADABLEFILE']._serialized_start=3505 + _globals['_DOWNLOADABLEFILE']._serialized_end=3625 + _globals['_SERVERERRORRESPONSE']._serialized_start=3628 + _globals['_SERVERERRORRESPONSE']._serialized_end=3781 + _globals['_RETRYINFO']._serialized_start=3783 + _globals['_RETRYINFO']._serialized_end=3827 + _globals['_SERVERTOAGENTCOMMAND']._serialized_start=3829 + _globals['_SERVERTOAGENTCOMMAND']._serialized_end=3891 + _globals['_AGENTDESCRIPTION']._serialized_start=3894 + _globals['_AGENTDESCRIPTION']._serialized_end=4026 + _globals['_COMPONENTHEALTH']._serialized_start=4029 + _globals['_COMPONENTHEALTH']._serialized_end=4333 + _globals['_COMPONENTHEALTH_COMPONENTHEALTHMAPENTRY']._serialized_start=4246 + _globals['_COMPONENTHEALTH_COMPONENTHEALTHMAPENTRY']._serialized_end=4333 + _globals['_EFFECTIVECONFIG']._serialized_start=4335 + _globals['_EFFECTIVECONFIG']._serialized_end=4401 + _globals['_REMOTECONFIGSTATUS']._serialized_start=4403 + _globals['_REMOTECONFIGSTATUS']._serialized_end=4530 + _globals['_PACKAGESTATUSES']._serialized_start=4533 + _globals['_PACKAGESTATUSES']._serialized_end=4755 + _globals['_PACKAGESTATUSES_PACKAGESENTRY']._serialized_start=4680 + _globals['_PACKAGESTATUSES_PACKAGESENTRY']._serialized_end=4755 + _globals['_PACKAGESTATUS']._serialized_start=4758 + _globals['_PACKAGESTATUS']._serialized_end=5033 + _globals['_PACKAGEDOWNLOADDETAILS']._serialized_start=5035 + _globals['_PACKAGEDOWNLOADDETAILS']._serialized_end=5120 + _globals['_AGENTIDENTIFICATION']._serialized_start=5122 + _globals['_AGENTIDENTIFICATION']._serialized_end=5169 + _globals['_AGENTREMOTECONFIG']._serialized_start=5171 + _globals['_AGENTREMOTECONFIG']._serialized_end=5256 + _globals['_AGENTCONFIGMAP']._serialized_start=5259 + _globals['_AGENTCONFIGMAP']._serialized_end=5419 + _globals['_AGENTCONFIGMAP_CONFIGMAPENTRY']._serialized_start=5341 + _globals['_AGENTCONFIGMAP_CONFIGMAPENTRY']._serialized_end=5419 + _globals['_AGENTCONFIGFILE']._serialized_start=5421 + _globals['_AGENTCONFIGFILE']._serialized_end=5474 + _globals['_CUSTOMCAPABILITIES']._serialized_start=5476 + _globals['_CUSTOMCAPABILITIES']._serialized_end=5518 + _globals['_CUSTOMMESSAGE']._serialized_start=5520 + _globals['_CUSTOMMESSAGE']._serialized_end=5583 +# @@protoc_insertion_point(module_scope) diff --git a/src/opentelemetry/_opamp/proto/opamp_pb2.pyi b/src/opentelemetry/_opamp/proto/opamp_pb2.pyi new file mode 100644 index 00000000..1f0ad4a2 --- /dev/null +++ b/src/opentelemetry/_opamp/proto/opamp_pb2.pyi @@ -0,0 +1,1987 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +OpAMP: Open Agent Management Protocol (https://github.com/open-telemetry/opamp-spec)""" +import anyvalue_pb2 +import builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _AgentToServerFlags: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _AgentToServerFlagsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_AgentToServerFlags.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + AgentToServerFlags_Unspecified: _AgentToServerFlags.ValueType # 0 + AgentToServerFlags_RequestInstanceUid: _AgentToServerFlags.ValueType # 1 + """AgentToServerFlags is a bit mask. Values below define individual bits. + + The Agent requests Server go generate a new instance_uid, which will + be sent back in ServerToAgent message + """ + +class AgentToServerFlags(_AgentToServerFlags, metaclass=_AgentToServerFlagsEnumTypeWrapper): ... + +AgentToServerFlags_Unspecified: AgentToServerFlags.ValueType # 0 +AgentToServerFlags_RequestInstanceUid: AgentToServerFlags.ValueType # 1 +"""AgentToServerFlags is a bit mask. Values below define individual bits. + +The Agent requests Server go generate a new instance_uid, which will +be sent back in ServerToAgent message +""" +global___AgentToServerFlags = AgentToServerFlags + +class _ServerToAgentFlags: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _ServerToAgentFlagsEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_ServerToAgentFlags.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + ServerToAgentFlags_Unspecified: _ServerToAgentFlags.ValueType # 0 + ServerToAgentFlags_ReportFullState: _ServerToAgentFlags.ValueType # 1 + """Flags is a bit mask. Values below define individual bits. + + ReportFullState flag can be used by the Server if the Agent did not include the + particular bit of information in the last status report (which is an allowed + optimization) but the Server detects that it does not have it (e.g. was + restarted and lost state). The detection happens using + AgentToServer.sequence_num values. + The Server asks the Agent to report full status. + """ + ServerToAgentFlags_ReportAvailableComponents: _ServerToAgentFlags.ValueType # 2 + """ReportAvailableComponents flag can be used by the server if the Agent did + not include the full AvailableComponents message, but only the hash. + If this flag is specified, the agent will populate available_components.components + with a full description of the agent's components. + Status: [Development] + """ + +class ServerToAgentFlags(_ServerToAgentFlags, metaclass=_ServerToAgentFlagsEnumTypeWrapper): ... + +ServerToAgentFlags_Unspecified: ServerToAgentFlags.ValueType # 0 +ServerToAgentFlags_ReportFullState: ServerToAgentFlags.ValueType # 1 +"""Flags is a bit mask. Values below define individual bits. + +ReportFullState flag can be used by the Server if the Agent did not include the +particular bit of information in the last status report (which is an allowed +optimization) but the Server detects that it does not have it (e.g. was +restarted and lost state). The detection happens using +AgentToServer.sequence_num values. +The Server asks the Agent to report full status. +""" +ServerToAgentFlags_ReportAvailableComponents: ServerToAgentFlags.ValueType # 2 +"""ReportAvailableComponents flag can be used by the server if the Agent did +not include the full AvailableComponents message, but only the hash. +If this flag is specified, the agent will populate available_components.components +with a full description of the agent's components. +Status: [Development] +""" +global___ServerToAgentFlags = ServerToAgentFlags + +class _ServerCapabilities: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _ServerCapabilitiesEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_ServerCapabilities.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + ServerCapabilities_Unspecified: _ServerCapabilities.ValueType # 0 + """The capabilities field is unspecified.""" + ServerCapabilities_AcceptsStatus: _ServerCapabilities.ValueType # 1 + """The Server can accept status reports. This bit MUST be set, since all Server + MUST be able to accept status reports. + """ + ServerCapabilities_OffersRemoteConfig: _ServerCapabilities.ValueType # 2 + """The Server can offer remote configuration to the Agent.""" + ServerCapabilities_AcceptsEffectiveConfig: _ServerCapabilities.ValueType # 4 + """The Server can accept EffectiveConfig in AgentToServer.""" + ServerCapabilities_OffersPackages: _ServerCapabilities.ValueType # 8 + """The Server can offer Packages. + Status: [Beta] + """ + ServerCapabilities_AcceptsPackagesStatus: _ServerCapabilities.ValueType # 16 + """The Server can accept Packages status. + Status: [Beta] + """ + ServerCapabilities_OffersConnectionSettings: _ServerCapabilities.ValueType # 32 + """The Server can offer connection settings. + Status: [Beta] + """ + ServerCapabilities_AcceptsConnectionSettingsRequest: _ServerCapabilities.ValueType # 64 + """The Server can accept ConnectionSettingsRequest and respond with an offer. + Status: [Development] + """ + +class ServerCapabilities(_ServerCapabilities, metaclass=_ServerCapabilitiesEnumTypeWrapper): ... + +ServerCapabilities_Unspecified: ServerCapabilities.ValueType # 0 +"""The capabilities field is unspecified.""" +ServerCapabilities_AcceptsStatus: ServerCapabilities.ValueType # 1 +"""The Server can accept status reports. This bit MUST be set, since all Server +MUST be able to accept status reports. +""" +ServerCapabilities_OffersRemoteConfig: ServerCapabilities.ValueType # 2 +"""The Server can offer remote configuration to the Agent.""" +ServerCapabilities_AcceptsEffectiveConfig: ServerCapabilities.ValueType # 4 +"""The Server can accept EffectiveConfig in AgentToServer.""" +ServerCapabilities_OffersPackages: ServerCapabilities.ValueType # 8 +"""The Server can offer Packages. +Status: [Beta] +""" +ServerCapabilities_AcceptsPackagesStatus: ServerCapabilities.ValueType # 16 +"""The Server can accept Packages status. +Status: [Beta] +""" +ServerCapabilities_OffersConnectionSettings: ServerCapabilities.ValueType # 32 +"""The Server can offer connection settings. +Status: [Beta] +""" +ServerCapabilities_AcceptsConnectionSettingsRequest: ServerCapabilities.ValueType # 64 +"""The Server can accept ConnectionSettingsRequest and respond with an offer. +Status: [Development] +""" +global___ServerCapabilities = ServerCapabilities + +class _PackageType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _PackageTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_PackageType.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + PackageType_TopLevel: _PackageType.ValueType # 0 + PackageType_Addon: _PackageType.ValueType # 1 + +class PackageType(_PackageType, metaclass=_PackageTypeEnumTypeWrapper): + """The type of the package, either an addon or a top-level package. + Status: [Beta] + """ + +PackageType_TopLevel: PackageType.ValueType # 0 +PackageType_Addon: PackageType.ValueType # 1 +global___PackageType = PackageType + +class _ServerErrorResponseType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _ServerErrorResponseTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_ServerErrorResponseType.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + ServerErrorResponseType_Unknown: _ServerErrorResponseType.ValueType # 0 + """Unknown error. Something went wrong, but it is not known what exactly. + The Agent SHOULD NOT retry the message. + The error_message field may contain a description of the problem. + """ + ServerErrorResponseType_BadRequest: _ServerErrorResponseType.ValueType # 1 + """The AgentToServer message was malformed. The Agent SHOULD NOT retry + the message. + """ + ServerErrorResponseType_Unavailable: _ServerErrorResponseType.ValueType # 2 + """The Server is overloaded and unable to process the request. The Agent + should retry the message later. retry_info field may be optionally + set with additional information about retrying. + """ + +class ServerErrorResponseType(_ServerErrorResponseType, metaclass=_ServerErrorResponseTypeEnumTypeWrapper): ... + +ServerErrorResponseType_Unknown: ServerErrorResponseType.ValueType # 0 +"""Unknown error. Something went wrong, but it is not known what exactly. +The Agent SHOULD NOT retry the message. +The error_message field may contain a description of the problem. +""" +ServerErrorResponseType_BadRequest: ServerErrorResponseType.ValueType # 1 +"""The AgentToServer message was malformed. The Agent SHOULD NOT retry +the message. +""" +ServerErrorResponseType_Unavailable: ServerErrorResponseType.ValueType # 2 +"""The Server is overloaded and unable to process the request. The Agent +should retry the message later. retry_info field may be optionally +set with additional information about retrying. +""" +global___ServerErrorResponseType = ServerErrorResponseType + +class _CommandType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _CommandTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_CommandType.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + CommandType_Restart: _CommandType.ValueType # 0 + """The Agent should restart. This request will be ignored if the Agent does not + support restart. + """ + +class CommandType(_CommandType, metaclass=_CommandTypeEnumTypeWrapper): + """Status: [Beta]""" + +CommandType_Restart: CommandType.ValueType # 0 +"""The Agent should restart. This request will be ignored if the Agent does not +support restart. +""" +global___CommandType = CommandType + +class _AgentCapabilities: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _AgentCapabilitiesEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_AgentCapabilities.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + AgentCapabilities_Unspecified: _AgentCapabilities.ValueType # 0 + """The capabilities field is unspecified.""" + AgentCapabilities_ReportsStatus: _AgentCapabilities.ValueType # 1 + """The Agent can report status. This bit MUST be set, since all Agents MUST + report status. + """ + AgentCapabilities_AcceptsRemoteConfig: _AgentCapabilities.ValueType # 2 + """The Agent can accept remote configuration from the Server.""" + AgentCapabilities_ReportsEffectiveConfig: _AgentCapabilities.ValueType # 4 + """The Agent will report EffectiveConfig in AgentToServer.""" + AgentCapabilities_AcceptsPackages: _AgentCapabilities.ValueType # 8 + """The Agent can accept package offers. + Status: [Beta] + """ + AgentCapabilities_ReportsPackageStatuses: _AgentCapabilities.ValueType # 16 + """The Agent can report package status. + Status: [Beta] + """ + AgentCapabilities_ReportsOwnTraces: _AgentCapabilities.ValueType # 32 + """The Agent can report own trace to the destination specified by + the Server via ConnectionSettingsOffers.own_traces field. + Status: [Beta] + """ + AgentCapabilities_ReportsOwnMetrics: _AgentCapabilities.ValueType # 64 + """The Agent can report own metrics to the destination specified by + the Server via ConnectionSettingsOffers.own_metrics field. + Status: [Beta] + """ + AgentCapabilities_ReportsOwnLogs: _AgentCapabilities.ValueType # 128 + """The Agent can report own logs to the destination specified by + the Server via ConnectionSettingsOffers.own_logs field. + Status: [Beta] + """ + AgentCapabilities_AcceptsOpAMPConnectionSettings: _AgentCapabilities.ValueType # 256 + """The can accept connections settings for OpAMP via + ConnectionSettingsOffers.opamp field. + Status: [Beta] + """ + AgentCapabilities_AcceptsOtherConnectionSettings: _AgentCapabilities.ValueType # 512 + """The can accept connections settings for other destinations via + ConnectionSettingsOffers.other_connections field. + Status: [Beta] + """ + AgentCapabilities_AcceptsRestartCommand: _AgentCapabilities.ValueType # 1024 + """The Agent can accept restart requests. + Status: [Beta] + """ + AgentCapabilities_ReportsHealth: _AgentCapabilities.ValueType # 2048 + """The Agent will report Health via AgentToServer.health field.""" + AgentCapabilities_ReportsRemoteConfig: _AgentCapabilities.ValueType # 4096 + """The Agent will report RemoteConfig status via AgentToServer.remote_config_status field.""" + AgentCapabilities_ReportsHeartbeat: _AgentCapabilities.ValueType # 8192 + """The Agent can report heartbeats. + This is specified by the ServerToAgent.OpAMPConnectionSettings.heartbeat_interval_seconds field. + If this capability is true, but the Server does not set a heartbeat_interval_seconds field, the + Agent should use its own configured interval, which by default will be 30s. The Server may not + know the configured interval and should not make assumptions about it. + Status: [Development] + """ + AgentCapabilities_ReportsAvailableComponents: _AgentCapabilities.ValueType # 16384 + """The agent will report AvailableComponents via the AgentToServer.available_components field. + Status: [Development] + Add new capabilities here, continuing with the least significant unused bit. + """ + +class AgentCapabilities(_AgentCapabilities, metaclass=_AgentCapabilitiesEnumTypeWrapper): ... + +AgentCapabilities_Unspecified: AgentCapabilities.ValueType # 0 +"""The capabilities field is unspecified.""" +AgentCapabilities_ReportsStatus: AgentCapabilities.ValueType # 1 +"""The Agent can report status. This bit MUST be set, since all Agents MUST +report status. +""" +AgentCapabilities_AcceptsRemoteConfig: AgentCapabilities.ValueType # 2 +"""The Agent can accept remote configuration from the Server.""" +AgentCapabilities_ReportsEffectiveConfig: AgentCapabilities.ValueType # 4 +"""The Agent will report EffectiveConfig in AgentToServer.""" +AgentCapabilities_AcceptsPackages: AgentCapabilities.ValueType # 8 +"""The Agent can accept package offers. +Status: [Beta] +""" +AgentCapabilities_ReportsPackageStatuses: AgentCapabilities.ValueType # 16 +"""The Agent can report package status. +Status: [Beta] +""" +AgentCapabilities_ReportsOwnTraces: AgentCapabilities.ValueType # 32 +"""The Agent can report own trace to the destination specified by +the Server via ConnectionSettingsOffers.own_traces field. +Status: [Beta] +""" +AgentCapabilities_ReportsOwnMetrics: AgentCapabilities.ValueType # 64 +"""The Agent can report own metrics to the destination specified by +the Server via ConnectionSettingsOffers.own_metrics field. +Status: [Beta] +""" +AgentCapabilities_ReportsOwnLogs: AgentCapabilities.ValueType # 128 +"""The Agent can report own logs to the destination specified by +the Server via ConnectionSettingsOffers.own_logs field. +Status: [Beta] +""" +AgentCapabilities_AcceptsOpAMPConnectionSettings: AgentCapabilities.ValueType # 256 +"""The can accept connections settings for OpAMP via +ConnectionSettingsOffers.opamp field. +Status: [Beta] +""" +AgentCapabilities_AcceptsOtherConnectionSettings: AgentCapabilities.ValueType # 512 +"""The can accept connections settings for other destinations via +ConnectionSettingsOffers.other_connections field. +Status: [Beta] +""" +AgentCapabilities_AcceptsRestartCommand: AgentCapabilities.ValueType # 1024 +"""The Agent can accept restart requests. +Status: [Beta] +""" +AgentCapabilities_ReportsHealth: AgentCapabilities.ValueType # 2048 +"""The Agent will report Health via AgentToServer.health field.""" +AgentCapabilities_ReportsRemoteConfig: AgentCapabilities.ValueType # 4096 +"""The Agent will report RemoteConfig status via AgentToServer.remote_config_status field.""" +AgentCapabilities_ReportsHeartbeat: AgentCapabilities.ValueType # 8192 +"""The Agent can report heartbeats. +This is specified by the ServerToAgent.OpAMPConnectionSettings.heartbeat_interval_seconds field. +If this capability is true, but the Server does not set a heartbeat_interval_seconds field, the +Agent should use its own configured interval, which by default will be 30s. The Server may not +know the configured interval and should not make assumptions about it. +Status: [Development] +""" +AgentCapabilities_ReportsAvailableComponents: AgentCapabilities.ValueType # 16384 +"""The agent will report AvailableComponents via the AgentToServer.available_components field. +Status: [Development] +Add new capabilities here, continuing with the least significant unused bit. +""" +global___AgentCapabilities = AgentCapabilities + +class _RemoteConfigStatuses: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _RemoteConfigStatusesEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_RemoteConfigStatuses.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + RemoteConfigStatuses_UNSET: _RemoteConfigStatuses.ValueType # 0 + """The value of status field is not set.""" + RemoteConfigStatuses_APPLIED: _RemoteConfigStatuses.ValueType # 1 + """Remote config was successfully applied by the Agent.""" + RemoteConfigStatuses_APPLYING: _RemoteConfigStatuses.ValueType # 2 + """Agent is currently applying the remote config that it received earlier.""" + RemoteConfigStatuses_FAILED: _RemoteConfigStatuses.ValueType # 3 + """Agent tried to apply the config received earlier, but it failed. + See error_message for more details. + """ + +class RemoteConfigStatuses(_RemoteConfigStatuses, metaclass=_RemoteConfigStatusesEnumTypeWrapper): ... + +RemoteConfigStatuses_UNSET: RemoteConfigStatuses.ValueType # 0 +"""The value of status field is not set.""" +RemoteConfigStatuses_APPLIED: RemoteConfigStatuses.ValueType # 1 +"""Remote config was successfully applied by the Agent.""" +RemoteConfigStatuses_APPLYING: RemoteConfigStatuses.ValueType # 2 +"""Agent is currently applying the remote config that it received earlier.""" +RemoteConfigStatuses_FAILED: RemoteConfigStatuses.ValueType # 3 +"""Agent tried to apply the config received earlier, but it failed. +See error_message for more details. +""" +global___RemoteConfigStatuses = RemoteConfigStatuses + +class _PackageStatusEnum: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _PackageStatusEnumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_PackageStatusEnum.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + PackageStatusEnum_Installed: _PackageStatusEnum.ValueType # 0 + """Package is successfully installed by the Agent. + The error_message field MUST NOT be set. + """ + PackageStatusEnum_InstallPending: _PackageStatusEnum.ValueType # 1 + """Installation of this package has not yet started.""" + PackageStatusEnum_Installing: _PackageStatusEnum.ValueType # 2 + """Agent is currently installing the package. + server_offered_hash field MUST be set to indicate the version that the + Agent is installing. The error_message field MUST NOT be set. + """ + PackageStatusEnum_InstallFailed: _PackageStatusEnum.ValueType # 3 + """Agent tried to install the package but installation failed. + server_offered_hash field MUST be set to indicate the version that the Agent + tried to install. The error_message may also contain more details about + the failure. + """ + PackageStatusEnum_Downloading: _PackageStatusEnum.ValueType # 4 + """Agent is currently downloading the package. + server_offered_hash field MUST be set to indicate the version that the + Agent is installing. The error_message field MUST NOT be set. + Status: [Development] + """ + +class PackageStatusEnum(_PackageStatusEnum, metaclass=_PackageStatusEnumEnumTypeWrapper): + """The status of this package. + Status: [Beta] + """ + +PackageStatusEnum_Installed: PackageStatusEnum.ValueType # 0 +"""Package is successfully installed by the Agent. +The error_message field MUST NOT be set. +""" +PackageStatusEnum_InstallPending: PackageStatusEnum.ValueType # 1 +"""Installation of this package has not yet started.""" +PackageStatusEnum_Installing: PackageStatusEnum.ValueType # 2 +"""Agent is currently installing the package. +server_offered_hash field MUST be set to indicate the version that the +Agent is installing. The error_message field MUST NOT be set. +""" +PackageStatusEnum_InstallFailed: PackageStatusEnum.ValueType # 3 +"""Agent tried to install the package but installation failed. +server_offered_hash field MUST be set to indicate the version that the Agent +tried to install. The error_message may also contain more details about +the failure. +""" +PackageStatusEnum_Downloading: PackageStatusEnum.ValueType # 4 +"""Agent is currently downloading the package. +server_offered_hash field MUST be set to indicate the version that the +Agent is installing. The error_message field MUST NOT be set. +Status: [Development] +""" +global___PackageStatusEnum = PackageStatusEnum + +@typing_extensions.final +class AgentToServer(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + INSTANCE_UID_FIELD_NUMBER: builtins.int + SEQUENCE_NUM_FIELD_NUMBER: builtins.int + AGENT_DESCRIPTION_FIELD_NUMBER: builtins.int + CAPABILITIES_FIELD_NUMBER: builtins.int + HEALTH_FIELD_NUMBER: builtins.int + EFFECTIVE_CONFIG_FIELD_NUMBER: builtins.int + REMOTE_CONFIG_STATUS_FIELD_NUMBER: builtins.int + PACKAGE_STATUSES_FIELD_NUMBER: builtins.int + AGENT_DISCONNECT_FIELD_NUMBER: builtins.int + FLAGS_FIELD_NUMBER: builtins.int + CONNECTION_SETTINGS_REQUEST_FIELD_NUMBER: builtins.int + CUSTOM_CAPABILITIES_FIELD_NUMBER: builtins.int + CUSTOM_MESSAGE_FIELD_NUMBER: builtins.int + AVAILABLE_COMPONENTS_FIELD_NUMBER: builtins.int + instance_uid: builtins.bytes + """Globally unique identifier of the running instance of the Agent. SHOULD remain + unchanged for the lifetime of the Agent process. + MUST be 16 bytes long and SHOULD be generated using the UUID v7 spec. + """ + sequence_num: builtins.int + """The sequence number is incremented by 1 for every AgentToServer sent + by the Agent. This allows the Server to detect that it missed a message when + it notices that the sequence_num is not exactly by 1 greater than the previously + received one. + """ + @property + def agent_description(self) -> global___AgentDescription: + """Data that describes the Agent, its type, where it runs, etc. + May be omitted if nothing changed since last AgentToServer message. + """ + capabilities: builtins.int + """Bitmask of flags defined by AgentCapabilities enum. + All bits that are not defined in AgentCapabilities enum MUST be set to 0 by + the Agent. This allows extending the protocol and the AgentCapabilities enum + in the future such that old Agents automatically report that they don't + support the new capability. + This field MUST be always set. + """ + @property + def health(self) -> global___ComponentHealth: + """The current health of the Agent and sub-components. The top-level ComponentHealth represents + the health of the Agent overall. May be omitted if nothing changed since last AgentToServer + message. + Status: [Beta] + """ + @property + def effective_config(self) -> global___EffectiveConfig: + """The current effective configuration of the Agent. The effective configuration is + the one that is currently used by the Agent. The effective configuration may be + different from the remote configuration received from the Server earlier, e.g. + because the Agent uses a local configuration instead (or in addition). + + This field SHOULD be unset if the effective config is unchanged since the last + AgentToServer message. + """ + @property + def remote_config_status(self) -> global___RemoteConfigStatus: + """The status of the remote config that was previously received from the Server. + This field SHOULD be unset if the remote config status is unchanged since the + last AgentToServer message. + """ + @property + def package_statuses(self) -> global___PackageStatuses: + """The list of the Agent packages, including package statuses. This field SHOULD be + unset if this information is unchanged since the last AgentToServer message for + this Agent was sent in the stream. + Status: [Beta] + """ + @property + def agent_disconnect(self) -> global___AgentDisconnect: + """AgentDisconnect MUST be set in the last AgentToServer message sent from the + Agent to the Server. + """ + flags: builtins.int + """Bit flags as defined by AgentToServerFlags bit masks.""" + @property + def connection_settings_request(self) -> global___ConnectionSettingsRequest: + """A request to create connection settings. This field is set for flows where + the Agent initiates the creation of connection settings. + Status: [Development] + """ + @property + def custom_capabilities(self) -> global___CustomCapabilities: + """A message indicating custom capabilities supported by the Agent. + Status: [Development] + """ + @property + def custom_message(self) -> global___CustomMessage: + """A custom message sent from an Agent to the Server. + Status: [Development] + """ + @property + def available_components(self) -> global___AvailableComponents: + """A message indicating the components that are available for configuration on the agent. + Status: [Development] + """ + def __init__( + self, + *, + instance_uid: builtins.bytes = ..., + sequence_num: builtins.int = ..., + agent_description: global___AgentDescription | None = ..., + capabilities: builtins.int = ..., + health: global___ComponentHealth | None = ..., + effective_config: global___EffectiveConfig | None = ..., + remote_config_status: global___RemoteConfigStatus | None = ..., + package_statuses: global___PackageStatuses | None = ..., + agent_disconnect: global___AgentDisconnect | None = ..., + flags: builtins.int = ..., + connection_settings_request: global___ConnectionSettingsRequest | None = ..., + custom_capabilities: global___CustomCapabilities | None = ..., + custom_message: global___CustomMessage | None = ..., + available_components: global___AvailableComponents | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["agent_description", b"agent_description", "agent_disconnect", b"agent_disconnect", "available_components", b"available_components", "connection_settings_request", b"connection_settings_request", "custom_capabilities", b"custom_capabilities", "custom_message", b"custom_message", "effective_config", b"effective_config", "health", b"health", "package_statuses", b"package_statuses", "remote_config_status", b"remote_config_status"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["agent_description", b"agent_description", "agent_disconnect", b"agent_disconnect", "available_components", b"available_components", "capabilities", b"capabilities", "connection_settings_request", b"connection_settings_request", "custom_capabilities", b"custom_capabilities", "custom_message", b"custom_message", "effective_config", b"effective_config", "flags", b"flags", "health", b"health", "instance_uid", b"instance_uid", "package_statuses", b"package_statuses", "remote_config_status", b"remote_config_status", "sequence_num", b"sequence_num"]) -> None: ... + +global___AgentToServer = AgentToServer + +@typing_extensions.final +class AgentDisconnect(google.protobuf.message.Message): + """AgentDisconnect is the last message sent from the Agent to the Server. The Server + SHOULD forget the association of the Agent instance with the message stream. + + If the message stream is closed in the transport layer then the Server SHOULD + forget association of all Agent instances that were previously established for + this message stream using AgentConnect message, even if the corresponding + AgentDisconnect message were not explicitly received from the Agent. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + def __init__( + self, + ) -> None: ... + +global___AgentDisconnect = AgentDisconnect + +@typing_extensions.final +class ConnectionSettingsRequest(google.protobuf.message.Message): + """ConnectionSettingsRequest is a request from the Agent to the Server to create + and respond with an offer of connection settings for the Agent. + Status: [Development] + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + OPAMP_FIELD_NUMBER: builtins.int + @property + def opamp(self) -> global___OpAMPConnectionSettingsRequest: + """Request for OpAMP connection settings. If this field is unset + then the ConnectionSettingsRequest message is empty and is not actionable + for the Server. + """ + def __init__( + self, + *, + opamp: global___OpAMPConnectionSettingsRequest | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["opamp", b"opamp"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["opamp", b"opamp"]) -> None: ... + +global___ConnectionSettingsRequest = ConnectionSettingsRequest + +@typing_extensions.final +class OpAMPConnectionSettingsRequest(google.protobuf.message.Message): + """OpAMPConnectionSettingsRequest is a request for the Server to produce + a OpAMPConnectionSettings in its response. + Status: [Development] + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CERTIFICATE_REQUEST_FIELD_NUMBER: builtins.int + @property + def certificate_request(self) -> global___CertificateRequest: + """A request to create a client certificate. This is used to initiate a + Client Signing Request (CSR) flow. + Required. + """ + def __init__( + self, + *, + certificate_request: global___CertificateRequest | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["certificate_request", b"certificate_request"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["certificate_request", b"certificate_request"]) -> None: ... + +global___OpAMPConnectionSettingsRequest = OpAMPConnectionSettingsRequest + +@typing_extensions.final +class CertificateRequest(google.protobuf.message.Message): + """Status: [Development]""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CSR_FIELD_NUMBER: builtins.int + csr: builtins.bytes + """PEM-encoded Client Certificate Signing Request (CSR), signed by client's private key. + The Server SHOULD validate the request and SHOULD respond with a + OpAMPConnectionSettings where the certificate.cert contains the issued + certificate. + """ + def __init__( + self, + *, + csr: builtins.bytes = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["csr", b"csr"]) -> None: ... + +global___CertificateRequest = CertificateRequest + +@typing_extensions.final +class AvailableComponents(google.protobuf.message.Message): + """AvailableComponents contains metadata relating to the components included + within the agent. + status: [Development] + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing_extensions.final + class ComponentsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___ComponentDetails: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___ComponentDetails | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + COMPONENTS_FIELD_NUMBER: builtins.int + HASH_FIELD_NUMBER: builtins.int + @property + def components(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___ComponentDetails]: + """A map of a unique component ID to details about the component. + This may be omitted from the message if the server has not + explicitly requested it be sent by setting the ReportAvailableComponents + flag in the previous ServerToAgent message. + """ + hash: builtins.bytes + """Agent-calculated hash of the components. + This hash should be included in every AvailableComponents message. + """ + def __init__( + self, + *, + components: collections.abc.Mapping[builtins.str, global___ComponentDetails] | None = ..., + hash: builtins.bytes = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["components", b"components", "hash", b"hash"]) -> None: ... + +global___AvailableComponents = AvailableComponents + +@typing_extensions.final +class ComponentDetails(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing_extensions.final + class SubComponentMapEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___ComponentDetails: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___ComponentDetails | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + METADATA_FIELD_NUMBER: builtins.int + SUB_COMPONENT_MAP_FIELD_NUMBER: builtins.int + @property + def metadata(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[anyvalue_pb2.KeyValue]: + """Extra key/value pairs that may be used to describe the component. + The key/value pairs are according to semantic conventions, see: + https://opentelemetry.io/docs/specs/semconv/ + + For example, you may use the "code" semantic conventions to + report the location of the code for a specific component: + https://opentelemetry.io/docs/specs/semconv/attributes-registry/code/ + + Or you may use the "vcs" semantic conventions to report the + repository the component may be a part of: + https://opentelemetry.io/docs/specs/semconv/attributes-registry/vcs/ + """ + @property + def sub_component_map(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___ComponentDetails]: + """A map of component ID to sub components details. It can nest as deeply as needed to + describe the underlying system. + """ + def __init__( + self, + *, + metadata: collections.abc.Iterable[anyvalue_pb2.KeyValue] | None = ..., + sub_component_map: collections.abc.Mapping[builtins.str, global___ComponentDetails] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["metadata", b"metadata", "sub_component_map", b"sub_component_map"]) -> None: ... + +global___ComponentDetails = ComponentDetails + +@typing_extensions.final +class ServerToAgent(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + INSTANCE_UID_FIELD_NUMBER: builtins.int + ERROR_RESPONSE_FIELD_NUMBER: builtins.int + REMOTE_CONFIG_FIELD_NUMBER: builtins.int + CONNECTION_SETTINGS_FIELD_NUMBER: builtins.int + PACKAGES_AVAILABLE_FIELD_NUMBER: builtins.int + FLAGS_FIELD_NUMBER: builtins.int + CAPABILITIES_FIELD_NUMBER: builtins.int + AGENT_IDENTIFICATION_FIELD_NUMBER: builtins.int + COMMAND_FIELD_NUMBER: builtins.int + CUSTOM_CAPABILITIES_FIELD_NUMBER: builtins.int + CUSTOM_MESSAGE_FIELD_NUMBER: builtins.int + instance_uid: builtins.bytes + """Agent instance uid. MUST match the instance_uid field in AgentToServer message. + Used for multiplexing messages from/to multiple agents using one message stream. + """ + @property + def error_response(self) -> global___ServerErrorResponse: + """error_response is set if the Server wants to indicate that something went wrong + during processing of an AgentToServer message. If error_response is set then + all other fields below must be unset and vice versa, if any of the fields below is + set then error_response must be unset. + """ + @property + def remote_config(self) -> global___AgentRemoteConfig: + """remote_config field is set when the Server has a remote config offer for the Agent.""" + @property + def connection_settings(self) -> global___ConnectionSettingsOffers: + """This field is set when the Server wants the Agent to change one or more + of its client connection settings (destination, headers, certificate, etc). + Status: [Beta] + """ + @property + def packages_available(self) -> global___PackagesAvailable: + """This field is set when the Server has packages to offer to the Agent. + Status: [Beta] + """ + flags: builtins.int + """Bit flags as defined by ServerToAgentFlags bit masks.""" + capabilities: builtins.int + """Bitmask of flags defined by ServerCapabilities enum. + All bits that are not defined in ServerCapabilities enum MUST be set to 0 + by the Server. This allows extending the protocol and the ServerCapabilities + enum in the future such that old Servers automatically report that they + don't support the new capability. + This field MUST be set in the first ServerToAgent sent by the Server and MAY + be omitted in subsequent ServerToAgent messages by setting it to + UnspecifiedServerCapability value. + """ + @property + def agent_identification(self) -> global___AgentIdentification: + """Properties related to identification of the Agent, which can be overridden + by the Server if needed. + """ + @property + def command(self) -> global___ServerToAgentCommand: + """Allows the Server to instruct the Agent to perform a command, e.g. RESTART. This field should not be specified + with fields other than instance_uid and capabilities. If specified, other fields will be ignored and the command + will be performed. + Status: [Beta] + """ + @property + def custom_capabilities(self) -> global___CustomCapabilities: + """A message indicating custom capabilities supported by the Server. + Status: [Development] + """ + @property + def custom_message(self) -> global___CustomMessage: + """A custom message sent from the Server to an Agent. + Status: [Development] + """ + def __init__( + self, + *, + instance_uid: builtins.bytes = ..., + error_response: global___ServerErrorResponse | None = ..., + remote_config: global___AgentRemoteConfig | None = ..., + connection_settings: global___ConnectionSettingsOffers | None = ..., + packages_available: global___PackagesAvailable | None = ..., + flags: builtins.int = ..., + capabilities: builtins.int = ..., + agent_identification: global___AgentIdentification | None = ..., + command: global___ServerToAgentCommand | None = ..., + custom_capabilities: global___CustomCapabilities | None = ..., + custom_message: global___CustomMessage | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["agent_identification", b"agent_identification", "command", b"command", "connection_settings", b"connection_settings", "custom_capabilities", b"custom_capabilities", "custom_message", b"custom_message", "error_response", b"error_response", "packages_available", b"packages_available", "remote_config", b"remote_config"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["agent_identification", b"agent_identification", "capabilities", b"capabilities", "command", b"command", "connection_settings", b"connection_settings", "custom_capabilities", b"custom_capabilities", "custom_message", b"custom_message", "error_response", b"error_response", "flags", b"flags", "instance_uid", b"instance_uid", "packages_available", b"packages_available", "remote_config", b"remote_config"]) -> None: ... + +global___ServerToAgent = ServerToAgent + +@typing_extensions.final +class OpAMPConnectionSettings(google.protobuf.message.Message): + """The OpAMPConnectionSettings message is a collection of fields which comprise an + offer from the Server to the Agent to use the specified settings for OpAMP + connection. + Status: [Beta] + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + DESTINATION_ENDPOINT_FIELD_NUMBER: builtins.int + HEADERS_FIELD_NUMBER: builtins.int + CERTIFICATE_FIELD_NUMBER: builtins.int + HEARTBEAT_INTERVAL_SECONDS_FIELD_NUMBER: builtins.int + destination_endpoint: builtins.str + """OpAMP Server URL This MUST be a WebSocket or HTTP URL and MUST be non-empty, for + example: "wss://example.com:4318/v1/opamp" + """ + @property + def headers(self) -> global___Headers: + """Optional headers to use when connecting. Typically used to set access tokens or + other authorization headers. For HTTP-based protocols the Agent should + set these in the request headers. + For example: + key="Authorization", Value="Basic YWxhZGRpbjpvcGVuc2VzYW1l". + """ + @property + def certificate(self) -> global___TLSCertificate: + """The Agent should use the offered certificate to connect to the destination + from now on. If the Agent is able to validate and connect using the offered + certificate the Agent SHOULD forget any previous client certificates + for this connection. + This field is optional: if omitted the client SHOULD NOT use a client-side certificate. + This field can be used to perform a client certificate revocation/rotation. + """ + heartbeat_interval_seconds: builtins.int + """The Agent MUST periodically send an AgentToServer message if the + AgentCapabilities_ReportsHeartbeat capability is true. At a minimum the instance_uid + field MUST be set. + + An HTTP Client MUST use the value as polling interval, if heartbeat_interval_seconds is non-zero. + + A heartbeat is used to keep the connection active and inform the server that the Agent + is still alive and active. + + If this field has no value or is set to 0, the Agent should not send any heartbeats. + Status: [Development] + """ + def __init__( + self, + *, + destination_endpoint: builtins.str = ..., + headers: global___Headers | None = ..., + certificate: global___TLSCertificate | None = ..., + heartbeat_interval_seconds: builtins.int = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["certificate", b"certificate", "headers", b"headers"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["certificate", b"certificate", "destination_endpoint", b"destination_endpoint", "headers", b"headers", "heartbeat_interval_seconds", b"heartbeat_interval_seconds"]) -> None: ... + +global___OpAMPConnectionSettings = OpAMPConnectionSettings + +@typing_extensions.final +class TelemetryConnectionSettings(google.protobuf.message.Message): + """The TelemetryConnectionSettings message is a collection of fields which comprise an + offer from the Server to the Agent to use the specified settings for a network + connection to report own telemetry. + Status: [Beta] + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + DESTINATION_ENDPOINT_FIELD_NUMBER: builtins.int + HEADERS_FIELD_NUMBER: builtins.int + CERTIFICATE_FIELD_NUMBER: builtins.int + destination_endpoint: builtins.str + """The value MUST be a full URL an OTLP/HTTP/Protobuf receiver with path. Schema + SHOULD begin with "https://", for example "https://example.com:4318/v1/metrics" + The Agent MAY refuse to send the telemetry if the URL begins with "http://". + """ + @property + def headers(self) -> global___Headers: + """Optional headers to use when connecting. Typically used to set access tokens or + other authorization headers. For HTTP-based protocols the Agent should + set these in the request headers. + For example: + key="Authorization", Value="Basic YWxhZGRpbjpvcGVuc2VzYW1l". + """ + @property + def certificate(self) -> global___TLSCertificate: + """The Agent should use the offered certificate to connect to the destination + from now on. If the Agent is able to validate and connect using the offered + certificate the Agent SHOULD forget any previous client certificates + for this connection. + This field is optional: if omitted the client SHOULD NOT use a client-side certificate. + This field can be used to perform a client certificate revocation/rotation. + """ + def __init__( + self, + *, + destination_endpoint: builtins.str = ..., + headers: global___Headers | None = ..., + certificate: global___TLSCertificate | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["certificate", b"certificate", "headers", b"headers"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["certificate", b"certificate", "destination_endpoint", b"destination_endpoint", "headers", b"headers"]) -> None: ... + +global___TelemetryConnectionSettings = TelemetryConnectionSettings + +@typing_extensions.final +class OtherConnectionSettings(google.protobuf.message.Message): + """The OtherConnectionSettings message is a collection of fields which comprise an + offer from the Server to the Agent to use the specified settings for a network + connection. It is not required that all fields in this message are specified. + The Server may specify only some of the fields, in which case it means that + the Server offers the Agent to change only those fields, while keeping the + rest of the fields unchanged. + + For example the Server may send a ConnectionSettings message with only the + certificate field set, while all other fields are unset. This means that + the Server wants the Agent to use a new certificate and continue sending to + the destination it is currently sending using the current header and other + settings. + + For fields which reference other messages the field is considered unset + when the reference is unset. + + For primitive field (string) we rely on the "flags" to describe that the + field is not set (this is done to overcome the limitation of old protoc + compilers don't generate methods that allow to check for the presence of + the field. + Status: [Beta] + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing_extensions.final + class OtherSettingsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + DESTINATION_ENDPOINT_FIELD_NUMBER: builtins.int + HEADERS_FIELD_NUMBER: builtins.int + CERTIFICATE_FIELD_NUMBER: builtins.int + OTHER_SETTINGS_FIELD_NUMBER: builtins.int + destination_endpoint: builtins.str + """A URL, host:port or some other destination specifier.""" + @property + def headers(self) -> global___Headers: + """Optional headers to use when connecting. Typically used to set access tokens or + other authorization headers. For HTTP-based protocols the Agent should + set these in the request headers. + For example: + key="Authorization", Value="Basic YWxhZGRpbjpvcGVuc2VzYW1l". + """ + @property + def certificate(self) -> global___TLSCertificate: + """The Agent should use the offered certificate to connect to the destination + from now on. If the Agent is able to validate and connect using the offered + certificate the Agent SHOULD forget any previous client certificates + for this connection. + This field is optional: if omitted the client SHOULD NOT use a client-side certificate. + This field can be used to perform a client certificate revocation/rotation. + """ + @property + def other_settings(self) -> google.protobuf.internal.containers.ScalarMap[builtins.str, builtins.str]: + """Other connection settings. These are Agent-specific and are up to the Agent + interpret. + """ + def __init__( + self, + *, + destination_endpoint: builtins.str = ..., + headers: global___Headers | None = ..., + certificate: global___TLSCertificate | None = ..., + other_settings: collections.abc.Mapping[builtins.str, builtins.str] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["certificate", b"certificate", "headers", b"headers"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["certificate", b"certificate", "destination_endpoint", b"destination_endpoint", "headers", b"headers", "other_settings", b"other_settings"]) -> None: ... + +global___OtherConnectionSettings = OtherConnectionSettings + +@typing_extensions.final +class Headers(google.protobuf.message.Message): + """Status: [Beta]""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + HEADERS_FIELD_NUMBER: builtins.int + @property + def headers(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Header]: ... + def __init__( + self, + *, + headers: collections.abc.Iterable[global___Header] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["headers", b"headers"]) -> None: ... + +global___Headers = Headers + +@typing_extensions.final +class Header(google.protobuf.message.Message): + """Status: [Beta]""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + value: builtins.str + def __init__( + self, + *, + key: builtins.str = ..., + value: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + +global___Header = Header + +@typing_extensions.final +class TLSCertificate(google.protobuf.message.Message): + """Status: [Beta] + The (cert,private_key) pair should be issued and signed by a Certificate + Authority (CA) that the destination Server recognizes. + + It is highly recommended that the private key of the CA certificate is NOT + stored on the destination Server otherwise compromising the Server will allow + a malicious actor to issue valid Server certificates which will be automatically + trusted by all agents and will allow the actor to trivially MITM Agent-to-Server + traffic of all servers that use this CA certificate for their Server-side + certificates. + + Alternatively the certificate may be self-signed, assuming the Server can + verify the certificate. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CERT_FIELD_NUMBER: builtins.int + PRIVATE_KEY_FIELD_NUMBER: builtins.int + CA_CERT_FIELD_NUMBER: builtins.int + cert: builtins.bytes + """PEM-encoded certificate. Required.""" + private_key: builtins.bytes + """PEM-encoded private key of the certificate. Required.""" + ca_cert: builtins.bytes + """PEM-encoded certificate of the signing CA. + Optional. MUST be specified if the certificate is CA-signed. + Can be stored by TLS-terminating intermediary proxies in order to verify + the connecting client's certificate in the future. + It is not recommended that the Agent accepts this CA as an authority for + any purposes. + """ + def __init__( + self, + *, + cert: builtins.bytes = ..., + private_key: builtins.bytes = ..., + ca_cert: builtins.bytes = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["ca_cert", b"ca_cert", "cert", b"cert", "private_key", b"private_key"]) -> None: ... + +global___TLSCertificate = TLSCertificate + +@typing_extensions.final +class ConnectionSettingsOffers(google.protobuf.message.Message): + """Status: [Beta]""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing_extensions.final + class OtherConnectionsEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___OtherConnectionSettings: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___OtherConnectionSettings | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + HASH_FIELD_NUMBER: builtins.int + OPAMP_FIELD_NUMBER: builtins.int + OWN_METRICS_FIELD_NUMBER: builtins.int + OWN_TRACES_FIELD_NUMBER: builtins.int + OWN_LOGS_FIELD_NUMBER: builtins.int + OTHER_CONNECTIONS_FIELD_NUMBER: builtins.int + hash: builtins.bytes + """Hash of all settings, including settings that may be omitted from this message + because they are unchanged. + """ + @property + def opamp(self) -> global___OpAMPConnectionSettings: + """Settings to connect to the OpAMP Server. + If this field is not set then the Agent should assume that the settings are + unchanged and should continue using existing settings. + The Agent MUST verify the offered connection settings by actually connecting + before accepting the setting to ensure it does not loose access to the OpAMP + Server due to invalid settings. + """ + @property + def own_metrics(self) -> global___TelemetryConnectionSettings: + """Settings to connect to an OTLP metrics backend to send Agent's own metrics to. + If this field is not set then the Agent should assume that the settings + are unchanged. + + Once accepted the Agent should periodically send to the specified destination + its own metrics, i.e. metrics of the Agent process and any custom metrics that + describe the Agent state. + + All attributes specified in the identifying_attributes field in AgentDescription + message SHOULD be also specified in the Resource of the reported OTLP metrics. + + Attributes specified in the non_identifying_attributes field in + AgentDescription message may be also specified in the Resource of the reported + OTLP metrics, in which case they SHOULD have exactly the same values. + + Process metrics MUST follow the conventions for processes: + https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/process-metrics.md + """ + @property + def own_traces(self) -> global___TelemetryConnectionSettings: + """Similar to own_metrics, but for traces.""" + @property + def own_logs(self) -> global___TelemetryConnectionSettings: + """Similar to own_metrics, but for logs.""" + @property + def other_connections(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___OtherConnectionSettings]: + """Another set of connection settings, with a string name associated with each. + How the Agent uses these is Agent-specific. Typically the name represents + the name of the destination to connect to (as it is known to the Agent). + If this field is not set then the Agent should assume that the other_connections + settings are unchanged. + """ + def __init__( + self, + *, + hash: builtins.bytes = ..., + opamp: global___OpAMPConnectionSettings | None = ..., + own_metrics: global___TelemetryConnectionSettings | None = ..., + own_traces: global___TelemetryConnectionSettings | None = ..., + own_logs: global___TelemetryConnectionSettings | None = ..., + other_connections: collections.abc.Mapping[builtins.str, global___OtherConnectionSettings] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["opamp", b"opamp", "own_logs", b"own_logs", "own_metrics", b"own_metrics", "own_traces", b"own_traces"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["hash", b"hash", "opamp", b"opamp", "other_connections", b"other_connections", "own_logs", b"own_logs", "own_metrics", b"own_metrics", "own_traces", b"own_traces"]) -> None: ... + +global___ConnectionSettingsOffers = ConnectionSettingsOffers + +@typing_extensions.final +class PackagesAvailable(google.protobuf.message.Message): + """List of packages that the Server offers to the Agent. + Status: [Beta] + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing_extensions.final + class PackagesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___PackageAvailable: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___PackageAvailable | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + PACKAGES_FIELD_NUMBER: builtins.int + ALL_PACKAGES_HASH_FIELD_NUMBER: builtins.int + @property + def packages(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___PackageAvailable]: + """Map of packages. Keys are package names, values are the packages available for download.""" + all_packages_hash: builtins.bytes + """Aggregate hash of all remotely installed packages. The Agent SHOULD include this + value in subsequent PackageStatuses messages. This in turn allows the management + Server to identify that a different set of packages is available for the Agent + and specify the available packages in the next ServerToAgent message. + + This field MUST be always set if the management Server supports packages + of agents. + + The hash is calculated as an aggregate of all packages names and content. + """ + def __init__( + self, + *, + packages: collections.abc.Mapping[builtins.str, global___PackageAvailable] | None = ..., + all_packages_hash: builtins.bytes = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["all_packages_hash", b"all_packages_hash", "packages", b"packages"]) -> None: ... + +global___PackagesAvailable = PackagesAvailable + +@typing_extensions.final +class PackageAvailable(google.protobuf.message.Message): + """Each Agent is composed of one or more packages. A package has a name and + content stored in a file. The content of the files, functionality + provided by the packages, how they are stored and used by the Agent side is Agent + type-specific and is outside the concerns of the OpAMP protocol. + + If the Agent does not have an installed package with the specified name then + it SHOULD download it from the specified URL and install it. + + If the Agent already has an installed package with the specified name + but with a different hash then the Agent SHOULD download and + install the package again, since it is a different version of the same package. + + If the Agent has an installed package with the specified name and the same + hash then the Agent does not need to do anything, it already + has the right version of the package. + Status: [Beta] + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TYPE_FIELD_NUMBER: builtins.int + VERSION_FIELD_NUMBER: builtins.int + FILE_FIELD_NUMBER: builtins.int + HASH_FIELD_NUMBER: builtins.int + type: global___PackageType.ValueType + version: builtins.str + """The package version that is available on the Server side. The Agent may for + example use this information to avoid downloading a package that was previously + already downloaded and failed to install. + """ + @property + def file(self) -> global___DownloadableFile: + """The downloadable file of the package.""" + hash: builtins.bytes + """The hash of the package. SHOULD be calculated based on all other fields of the + PackageAvailable message and content of the file of the package. The hash is + used by the Agent to determine if the package it has is different from the + package the Server is offering. + """ + def __init__( + self, + *, + type: global___PackageType.ValueType = ..., + version: builtins.str = ..., + file: global___DownloadableFile | None = ..., + hash: builtins.bytes = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["file", b"file"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["file", b"file", "hash", b"hash", "type", b"type", "version", b"version"]) -> None: ... + +global___PackageAvailable = PackageAvailable + +@typing_extensions.final +class DownloadableFile(google.protobuf.message.Message): + """Status: [Beta]""" + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + DOWNLOAD_URL_FIELD_NUMBER: builtins.int + CONTENT_HASH_FIELD_NUMBER: builtins.int + SIGNATURE_FIELD_NUMBER: builtins.int + HEADERS_FIELD_NUMBER: builtins.int + download_url: builtins.str + """The URL from which the file can be downloaded using HTTP GET request. + The Server at the specified URL SHOULD support range requests + to allow for resuming downloads. + """ + content_hash: builtins.bytes + """The hash of the file content. Can be used by the Agent to verify that the file + was downloaded correctly. + """ + signature: builtins.bytes + """Optional signature of the file content. Can be used by the Agent to verify the + authenticity of the downloaded file, for example can be the + [detached GPG signature](https://www.gnupg.org/gph/en/manual/x135.html#AEN160). + The exact signing and verification method is Agent specific. See + https://github.com/open-telemetry/opamp-spec/blob/main/specification.md#code-signing + for recommendations. + """ + @property + def headers(self) -> global___Headers: + """Optional headers to use when downloading a file. Typically used to set + access tokens or other authorization headers. For HTTP-based protocols + the Agent should set these in the request headers. + For example: + key="Authorization", Value="Basic YWxhZGRpbjpvcGVuc2VzYW1l". + Status: [Development] + """ + def __init__( + self, + *, + download_url: builtins.str = ..., + content_hash: builtins.bytes = ..., + signature: builtins.bytes = ..., + headers: global___Headers | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["headers", b"headers"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["content_hash", b"content_hash", "download_url", b"download_url", "headers", b"headers", "signature", b"signature"]) -> None: ... + +global___DownloadableFile = DownloadableFile + +@typing_extensions.final +class ServerErrorResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TYPE_FIELD_NUMBER: builtins.int + ERROR_MESSAGE_FIELD_NUMBER: builtins.int + RETRY_INFO_FIELD_NUMBER: builtins.int + type: global___ServerErrorResponseType.ValueType + error_message: builtins.str + """Error message in the string form, typically human readable.""" + @property + def retry_info(self) -> global___RetryInfo: + """Additional information about retrying if type==UNAVAILABLE.""" + def __init__( + self, + *, + type: global___ServerErrorResponseType.ValueType = ..., + error_message: builtins.str = ..., + retry_info: global___RetryInfo | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["Details", b"Details", "retry_info", b"retry_info"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["Details", b"Details", "error_message", b"error_message", "retry_info", b"retry_info", "type", b"type"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["Details", b"Details"]) -> typing_extensions.Literal["retry_info"] | None: ... + +global___ServerErrorResponse = ServerErrorResponse + +@typing_extensions.final +class RetryInfo(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + RETRY_AFTER_NANOSECONDS_FIELD_NUMBER: builtins.int + retry_after_nanoseconds: builtins.int + def __init__( + self, + *, + retry_after_nanoseconds: builtins.int = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["retry_after_nanoseconds", b"retry_after_nanoseconds"]) -> None: ... + +global___RetryInfo = RetryInfo + +@typing_extensions.final +class ServerToAgentCommand(google.protobuf.message.Message): + """ServerToAgentCommand is sent from the Server to the Agent to request that the Agent + perform a command. + Status: [Beta] + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TYPE_FIELD_NUMBER: builtins.int + type: global___CommandType.ValueType + def __init__( + self, + *, + type: global___CommandType.ValueType = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["type", b"type"]) -> None: ... + +global___ServerToAgentCommand = ServerToAgentCommand + +@typing_extensions.final +class AgentDescription(google.protobuf.message.Message): + """////////////////////////////////////////////////////////////////////////////////// + Status reporting + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + IDENTIFYING_ATTRIBUTES_FIELD_NUMBER: builtins.int + NON_IDENTIFYING_ATTRIBUTES_FIELD_NUMBER: builtins.int + @property + def identifying_attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[anyvalue_pb2.KeyValue]: + """Attributes that identify the Agent. + Keys/values are according to OpenTelemetry semantic conventions, see: + https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/resource/semantic_conventions + + For standalone running Agents (such as OpenTelemetry Collector) the following + attributes SHOULD be specified: + - service.name should be set to a reverse FQDN that uniquely identifies the + Agent type, e.g. "io.opentelemetry.collector" + - service.namespace if it is used in the environment where the Agent runs. + - service.version should be set to version number of the Agent build. + - service.instance.id should be set. It may be set equal to the Agent's + instance uid (equal to ServerToAgent.instance_uid field) or any other value + that uniquely identifies the Agent in combination with other attributes. + - any other attributes that are necessary for uniquely identifying the Agent's + own telemetry. + + The Agent SHOULD also include these attributes in the Resource of its own + telemetry. The combination of identifying attributes SHOULD be sufficient to + uniquely identify the Agent's own telemetry in the destination system to which + the Agent sends its own telemetry. + """ + @property + def non_identifying_attributes(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[anyvalue_pb2.KeyValue]: + """Attributes that do not necessarily identify the Agent but help describe + where it runs. + The following attributes SHOULD be included: + - os.type, os.version - to describe where the Agent runs. + - host.* to describe the host the Agent runs on. + - cloud.* to describe the cloud where the host is located. + - any other relevant Resource attributes that describe this Agent and the + environment it runs in. + - any user-defined attributes that the end user would like to associate + with this Agent. + """ + def __init__( + self, + *, + identifying_attributes: collections.abc.Iterable[anyvalue_pb2.KeyValue] | None = ..., + non_identifying_attributes: collections.abc.Iterable[anyvalue_pb2.KeyValue] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["identifying_attributes", b"identifying_attributes", "non_identifying_attributes", b"non_identifying_attributes"]) -> None: ... + +global___AgentDescription = AgentDescription + +@typing_extensions.final +class ComponentHealth(google.protobuf.message.Message): + """The health of the Agent and sub-components + Status: [Beta] + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing_extensions.final + class ComponentHealthMapEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___ComponentHealth: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___ComponentHealth | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + HEALTHY_FIELD_NUMBER: builtins.int + START_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + LAST_ERROR_FIELD_NUMBER: builtins.int + STATUS_FIELD_NUMBER: builtins.int + STATUS_TIME_UNIX_NANO_FIELD_NUMBER: builtins.int + COMPONENT_HEALTH_MAP_FIELD_NUMBER: builtins.int + healthy: builtins.bool + """Set to true if the component is up and healthy.""" + start_time_unix_nano: builtins.int + """Timestamp since the component is up, i.e. when the component was started. + Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + If the component is not running MUST be set to 0. + """ + last_error: builtins.str + """Human-readable error message if the component is in erroneous state. SHOULD be set + when healthy==false. + """ + status: builtins.str + """Component status represented as a string. The status values are defined by agent-specific + semantics and not at the protocol level. + """ + status_time_unix_nano: builtins.int + """The time when the component status was observed. Value is UNIX Epoch time in + nanoseconds since 00:00:00 UTC on 1 January 1970. + """ + @property + def component_health_map(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___ComponentHealth]: + """A map to store more granular, sub-component health. It can nest as deeply as needed to + describe the underlying system. + """ + def __init__( + self, + *, + healthy: builtins.bool = ..., + start_time_unix_nano: builtins.int = ..., + last_error: builtins.str = ..., + status: builtins.str = ..., + status_time_unix_nano: builtins.int = ..., + component_health_map: collections.abc.Mapping[builtins.str, global___ComponentHealth] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["component_health_map", b"component_health_map", "healthy", b"healthy", "last_error", b"last_error", "start_time_unix_nano", b"start_time_unix_nano", "status", b"status", "status_time_unix_nano", b"status_time_unix_nano"]) -> None: ... + +global___ComponentHealth = ComponentHealth + +@typing_extensions.final +class EffectiveConfig(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CONFIG_MAP_FIELD_NUMBER: builtins.int + @property + def config_map(self) -> global___AgentConfigMap: + """The effective config of the Agent.""" + def __init__( + self, + *, + config_map: global___AgentConfigMap | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["config_map", b"config_map"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["config_map", b"config_map"]) -> None: ... + +global___EffectiveConfig = EffectiveConfig + +@typing_extensions.final +class RemoteConfigStatus(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + LAST_REMOTE_CONFIG_HASH_FIELD_NUMBER: builtins.int + STATUS_FIELD_NUMBER: builtins.int + ERROR_MESSAGE_FIELD_NUMBER: builtins.int + last_remote_config_hash: builtins.bytes + """The hash of the remote config that was last received by this Agent in the + AgentRemoteConfig.config_hash field. + The Server SHOULD compare this hash with the config hash + it has for the Agent and if the hashes are different the Server MUST include + the remote_config field in the response in the ServerToAgent message. + """ + status: global___RemoteConfigStatuses.ValueType + error_message: builtins.str + """Optional error message if status==FAILED.""" + def __init__( + self, + *, + last_remote_config_hash: builtins.bytes = ..., + status: global___RemoteConfigStatuses.ValueType = ..., + error_message: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["error_message", b"error_message", "last_remote_config_hash", b"last_remote_config_hash", "status", b"status"]) -> None: ... + +global___RemoteConfigStatus = RemoteConfigStatus + +@typing_extensions.final +class PackageStatuses(google.protobuf.message.Message): + """The PackageStatuses message describes the status of all packages that the Agent + has or was offered. + Status: [Beta] + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing_extensions.final + class PackagesEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___PackageStatus: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___PackageStatus | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + PACKAGES_FIELD_NUMBER: builtins.int + SERVER_PROVIDED_ALL_PACKAGES_HASH_FIELD_NUMBER: builtins.int + ERROR_MESSAGE_FIELD_NUMBER: builtins.int + @property + def packages(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___PackageStatus]: + """A map of PackageStatus messages, where the keys are package names. + The key MUST match the name field of PackageStatus message. + """ + server_provided_all_packages_hash: builtins.bytes + """The aggregate hash of all packages that this Agent previously received from the + Server via PackagesAvailable message. + + The Server SHOULD compare this hash to the aggregate hash of all packages that + it has for this Agent and if the hashes are different the Server SHOULD send + an PackagesAvailable message to the Agent. + """ + error_message: builtins.str + """This field is set if the Agent encountered an error when processing the + PackagesAvailable message and that error is not related to any particular single + package. + The field must be unset is there were no processing errors. + """ + def __init__( + self, + *, + packages: collections.abc.Mapping[builtins.str, global___PackageStatus] | None = ..., + server_provided_all_packages_hash: builtins.bytes = ..., + error_message: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["error_message", b"error_message", "packages", b"packages", "server_provided_all_packages_hash", b"server_provided_all_packages_hash"]) -> None: ... + +global___PackageStatuses = PackageStatuses + +@typing_extensions.final +class PackageStatus(google.protobuf.message.Message): + """The status of a single package. + Status: [Beta] + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + AGENT_HAS_VERSION_FIELD_NUMBER: builtins.int + AGENT_HAS_HASH_FIELD_NUMBER: builtins.int + SERVER_OFFERED_VERSION_FIELD_NUMBER: builtins.int + SERVER_OFFERED_HASH_FIELD_NUMBER: builtins.int + STATUS_FIELD_NUMBER: builtins.int + ERROR_MESSAGE_FIELD_NUMBER: builtins.int + DOWNLOAD_DETAILS_FIELD_NUMBER: builtins.int + name: builtins.str + """Package name. MUST be always set and MUST match the key in the packages field + of PackageStatuses message. + """ + agent_has_version: builtins.str + """The version of the package that the Agent has. + MUST be set if the Agent has this package. + MUST be empty if the Agent does not have this package. This may be the case + for example if the package was offered by the Server but failed to install + and the Agent did not have this package previously. + """ + agent_has_hash: builtins.bytes + """The hash of the package that the Agent has. + MUST be set if the Agent has this package. + MUST be empty if the Agent does not have this package. This may be the case for + example if the package was offered by the Server but failed to install and the + Agent did not have this package previously. + """ + server_offered_version: builtins.str + """The version of the package that the Server offered to the Agent. + MUST be set if the installation of the package is initiated by an earlier offer + from the Server to install this package. + + MUST be empty if the Agent has this package but it was installed locally and + was not offered by the Server. + + Note that it is possible for both agent_has_version and server_offered_version + fields to be set and to have different values. This is for example possible if + the Agent already has a version of the package successfully installed, the Server + offers a different version, but the Agent fails to install that version. + """ + server_offered_hash: builtins.bytes + """The hash of the package that the Server offered to the Agent. + MUST be set if the installation of the package is initiated by an earlier + offer from the Server to install this package. + + MUST be empty if the Agent has this package but it was installed locally and + was not offered by the Server. + + Note that it is possible for both agent_has_hash and server_offered_hash + fields to be set and to have different values. This is for example possible if + the Agent already has a version of the package successfully installed, the + Server offers a different version, but the Agent fails to install that version. + """ + status: global___PackageStatusEnum.ValueType + error_message: builtins.str + """Error message if the status is erroneous.""" + @property + def download_details(self) -> global___PackageDownloadDetails: + """Optional details that may be of interest to a user. + Should only be set if status is Downloading. + Status: [Development] + """ + def __init__( + self, + *, + name: builtins.str = ..., + agent_has_version: builtins.str = ..., + agent_has_hash: builtins.bytes = ..., + server_offered_version: builtins.str = ..., + server_offered_hash: builtins.bytes = ..., + status: global___PackageStatusEnum.ValueType = ..., + error_message: builtins.str = ..., + download_details: global___PackageDownloadDetails | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["download_details", b"download_details"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["agent_has_hash", b"agent_has_hash", "agent_has_version", b"agent_has_version", "download_details", b"download_details", "error_message", b"error_message", "name", b"name", "server_offered_hash", b"server_offered_hash", "server_offered_version", b"server_offered_version", "status", b"status"]) -> None: ... + +global___PackageStatus = PackageStatus + +@typing_extensions.final +class PackageDownloadDetails(google.protobuf.message.Message): + """Additional details that an agent can use to describe an in-progress package download. + Status: [Development] + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + DOWNLOAD_PERCENT_FIELD_NUMBER: builtins.int + DOWNLOAD_BYTES_PER_SECOND_FIELD_NUMBER: builtins.int + download_percent: builtins.float + """The package download progress as a percentage.""" + download_bytes_per_second: builtins.float + """The current package download rate in bytes per second.""" + def __init__( + self, + *, + download_percent: builtins.float = ..., + download_bytes_per_second: builtins.float = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["download_bytes_per_second", b"download_bytes_per_second", "download_percent", b"download_percent"]) -> None: ... + +global___PackageDownloadDetails = PackageDownloadDetails + +@typing_extensions.final +class AgentIdentification(google.protobuf.message.Message): + """Properties related to identification of the Agent, which can be overridden + by the Server if needed + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NEW_INSTANCE_UID_FIELD_NUMBER: builtins.int + new_instance_uid: builtins.bytes + """When new_instance_uid is set, Agent MUST update instance_uid + to the value provided and use it for all further communication. + MUST be 16 bytes long and SHOULD be generated using the UUID v7 spec. + """ + def __init__( + self, + *, + new_instance_uid: builtins.bytes = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["new_instance_uid", b"new_instance_uid"]) -> None: ... + +global___AgentIdentification = AgentIdentification + +@typing_extensions.final +class AgentRemoteConfig(google.protobuf.message.Message): + """/////////////////////////////////////////////////////////////////////////////////// + Config messages + /////////////////////////////////////////////////////////////////////////////////// + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CONFIG_FIELD_NUMBER: builtins.int + CONFIG_HASH_FIELD_NUMBER: builtins.int + @property + def config(self) -> global___AgentConfigMap: + """Agent config offered by the management Server to the Agent instance. SHOULD NOT be + set if the config for this Agent has not changed since it was last requested (i.e. + AgentConfigRequest.last_remote_config_hash field is equal to + AgentConfigResponse.config_hash field). + """ + config_hash: builtins.bytes + """Hash of "config". The Agent SHOULD include this value in subsequent + RemoteConfigStatus messages in the last_remote_config_hash field. This in turn + allows the management Server to identify that a new config is available for the Agent. + + This field MUST be always set if the management Server supports remote configuration + of agents. + + Management Server must choose a hashing function that guarantees lack of hash + collisions in practice. + """ + def __init__( + self, + *, + config: global___AgentConfigMap | None = ..., + config_hash: builtins.bytes = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["config", b"config"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["config", b"config", "config_hash", b"config_hash"]) -> None: ... + +global___AgentRemoteConfig = AgentRemoteConfig + +@typing_extensions.final +class AgentConfigMap(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing_extensions.final + class ConfigMapEntry(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + key: builtins.str + @property + def value(self) -> global___AgentConfigFile: ... + def __init__( + self, + *, + key: builtins.str = ..., + value: global___AgentConfigFile | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "value", b"value"]) -> None: ... + + CONFIG_MAP_FIELD_NUMBER: builtins.int + @property + def config_map(self) -> google.protobuf.internal.containers.MessageMap[builtins.str, global___AgentConfigFile]: + """Map of configs. Keys are config file names or config section names. + The configuration is assumed to be a collection of one or more named config files + or sections. + For agents that use a single config file or section the map SHOULD contain a single + entry and the key may be an empty string. + """ + def __init__( + self, + *, + config_map: collections.abc.Mapping[builtins.str, global___AgentConfigFile] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["config_map", b"config_map"]) -> None: ... + +global___AgentConfigMap = AgentConfigMap + +@typing_extensions.final +class AgentConfigFile(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + BODY_FIELD_NUMBER: builtins.int + CONTENT_TYPE_FIELD_NUMBER: builtins.int + body: builtins.bytes + """Config file or section body. The content, format and encoding depends on the Agent + type. The content_type field may optionally describe the MIME type of the body. + """ + content_type: builtins.str + """Optional MIME Content-Type that describes what's in the body field, for + example "text/yaml". + """ + def __init__( + self, + *, + body: builtins.bytes = ..., + content_type: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["body", b"body", "content_type", b"content_type"]) -> None: ... + +global___AgentConfigFile = AgentConfigFile + +@typing_extensions.final +class CustomCapabilities(google.protobuf.message.Message): + """/////////////////////////////////////////////////////////////////////////////////// + Custom messages + /////////////////////////////////////////////////////////////////////////////////// + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CAPABILITIES_FIELD_NUMBER: builtins.int + @property + def capabilities(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """A list of custom capabilities that are supported. Each capability is a reverse FQDN + with optional version information that uniquely identifies the custom capability + and should match a capability specified in a supported CustomMessage. + Status: [Development] + """ + def __init__( + self, + *, + capabilities: collections.abc.Iterable[builtins.str] | None = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["capabilities", b"capabilities"]) -> None: ... + +global___CustomCapabilities = CustomCapabilities + +@typing_extensions.final +class CustomMessage(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + CAPABILITY_FIELD_NUMBER: builtins.int + TYPE_FIELD_NUMBER: builtins.int + DATA_FIELD_NUMBER: builtins.int + capability: builtins.str + """A reverse FQDN that uniquely identifies the capability and matches one of the + capabilities in the CustomCapabilities message. + Status: [Development] + """ + type: builtins.str + """Type of message within the capability. The capability defines the types of custom + messages that are used to implement the capability. The type must only be unique + within the capability. + Status: [Development] + """ + data: builtins.bytes + """Binary data of the message. The capability must specify the format of the contents + of the data for each custom message type it defines. + Status: [Development] + """ + def __init__( + self, + *, + capability: builtins.str = ..., + type: builtins.str = ..., + data: builtins.bytes = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["capability", b"capability", "data", b"data", "type", b"type"]) -> None: ... + +global___CustomMessage = CustomMessage diff --git a/src/opentelemetry/_opamp/transport/base.py b/src/opentelemetry/_opamp/transport/base.py new file mode 100644 index 00000000..1ca426c5 --- /dev/null +++ b/src/opentelemetry/_opamp/transport/base.py @@ -0,0 +1,31 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +from typing import Mapping + +from opentelemetry._opamp.proto import opamp_pb2 as opamp_pb2 + + +base_headers = { + "Content-Type": "application/x-protobuf", +} + + +class HttpTransport(abc.ABC): + @abc.abstractmethod + def send(self, url: str, headers: Mapping[str, str], data: bytes, timeout_millis: int) -> opamp_pb2.ServerToAgent: + pass diff --git a/src/opentelemetry/_opamp/transport/exceptions.py b/src/opentelemetry/_opamp/transport/exceptions.py new file mode 100644 index 00000000..942f573b --- /dev/null +++ b/src/opentelemetry/_opamp/transport/exceptions.py @@ -0,0 +1,19 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class OpAMPException(Exception): + pass diff --git a/src/opentelemetry/_opamp/transport/requests.py b/src/opentelemetry/_opamp/transport/requests.py new file mode 100644 index 00000000..975c6712 --- /dev/null +++ b/src/opentelemetry/_opamp/transport/requests.py @@ -0,0 +1,43 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Mapping + +import requests + +from opentelemetry._opamp import messages +from opentelemetry._opamp.transport.exceptions import OpAMPException +from opentelemetry._opamp.transport.base import HttpTransport, base_headers + + +class RequestsTransport(HttpTransport): + # TODO: move some stuff here instead of send? + def __init__(self): + self.session = requests.Session() + + # TODO: support basic-auth? + def send(self, url: str, headers: Mapping[str, str], data: bytes, timeout_millis: int): + headers = {**base_headers, **headers} + timeout: float = timeout_millis / 1e3 + try: + response = self.session.post(url, headers=headers, data=data, timeout=timeout) + response.raise_for_status() + except Exception: + raise OpAMPException + + message = messages._decode_message(response.content) + + return message diff --git a/src/opentelemetry/_opamp/version.py b/src/opentelemetry/_opamp/version.py new file mode 100644 index 00000000..d42491ed --- /dev/null +++ b/src/opentelemetry/_opamp/version.py @@ -0,0 +1,17 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = "0.0.1" diff --git a/tests/distro/test_distro.py b/tests/distro/test_distro.py index 40544f0a..3696c16f 100644 --- a/tests/distro/test_distro.py +++ b/tests/distro/test_distro.py @@ -14,11 +14,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json +import logging import os from unittest import TestCase, mock -from elasticotel.distro import ElasticOpenTelemetryDistro -from elasticotel.distro.environment_variables import ELASTIC_OTEL_SYSTEM_METRICS_ENABLED +from elasticotel.distro import ElasticOpenTelemetryConfigurator, ElasticOpenTelemetryDistro, logger as distro_logger +from elasticotel.distro.config import opamp_handler, logger as config_logger +from elasticotel.distro.environment_variables import ELASTIC_OTEL_OPAMP_ENDPOINT, ELASTIC_OTEL_SYSTEM_METRICS_ENABLED from opentelemetry.environment_variables import ( OTEL_LOGS_EXPORTER, OTEL_METRICS_EXPORTER, @@ -30,6 +33,7 @@ OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, OTEL_EXPORTER_OTLP_PROTOCOL, ) +from opentelemetry._opamp.proto import opamp_pb2 as opamp_pb2 class TestDistribution(TestCase): @@ -90,3 +94,189 @@ def test_load_instrumentor_default_kwargs_for_instrumentors(self): distro.load_instrumentor(entryPoint_mock) instrumentor_mock.assert_called_once_with() + + @mock.patch.dict( + "os.environ", + { + ELASTIC_OTEL_OPAMP_ENDPOINT: "http://localhost:4320/v1/opamp", + "OTEL_RESOURCE_ATTRIBUTES": "service.name=service,deployment.environment.name=dev", + }, + clear=True, + ) + @mock.patch("elasticotel.distro.OpAMPAgent") + @mock.patch("elasticotel.distro.OpAMPClient") + def test_configurator_sets_up_opamp_with_http_endpoint(self, client_mock, agent_mock): + client_mock.return_value = client_mock + agent_mock.return_value = agent_mock + + ElasticOpenTelemetryConfigurator()._configure() + + client_mock.assert_called_once_with( + endpoint="http://localhost:4320/v1/opamp", + agent_identifying_attributes={"service.name": "service", "deployment.environment.name": "dev"}, + ) + agent_mock.assert_called_once_with(interval=30, message_handler=opamp_handler, client=client_mock) + agent_mock.start.assert_called_once_with() + + @mock.patch.dict( + "os.environ", + { + ELASTIC_OTEL_OPAMP_ENDPOINT: "https://localhost:4320/v1/opamp", + "OTEL_RESOURCE_ATTRIBUTES": "service.name=service,deployment.environment.name=dev", + }, + clear=True, + ) + @mock.patch("elasticotel.distro.OpAMPAgent") + @mock.patch("elasticotel.distro.OpAMPClient") + def test_configurator_sets_up_opamp_with_https_endpoint(self, client_mock, agent_mock): + client_mock.return_value = client_mock + agent_mock.return_value = agent_mock + + ElasticOpenTelemetryConfigurator()._configure() + + client_mock.assert_called_once_with( + endpoint="https://localhost:4320/v1/opamp", + agent_identifying_attributes={"service.name": "service", "deployment.environment.name": "dev"}, + ) + agent_mock.assert_called_once_with(interval=30, message_handler=opamp_handler, client=client_mock) + agent_mock.start.assert_called_once_with() + + @mock.patch.dict( + "os.environ", + { + ELASTIC_OTEL_OPAMP_ENDPOINT: "https://localhost:4320", + "OTEL_RESOURCE_ATTRIBUTES": "service.name=service,deployment.environment.name=dev", + }, + clear=True, + ) + @mock.patch("elasticotel.distro.OpAMPAgent") + @mock.patch("elasticotel.distro.OpAMPClient") + def test_configurator_adds_path_to_opamp_endpoint_if_missing(self, client_mock, agent_mock): + client_mock.return_value = client_mock + agent_mock.return_value = agent_mock + + ElasticOpenTelemetryConfigurator()._configure() + + client_mock.assert_called_once_with( + endpoint="https://localhost:4320/v1/opamp", + agent_identifying_attributes={"service.name": "service", "deployment.environment.name": "dev"}, + ) + agent_mock.assert_called_once_with(interval=30, message_handler=opamp_handler, client=client_mock) + agent_mock.start.assert_called_once_with() + + @mock.patch.dict( + "os.environ", + { + ELASTIC_OTEL_OPAMP_ENDPOINT: "https://localhost:4320/v1/opamp", + "OTEL_RESOURCE_ATTRIBUTES": "service.name=service", + }, + clear=True, + ) + @mock.patch("elasticotel.distro.OpAMPAgent") + @mock.patch("elasticotel.distro.OpAMPClient") + def test_configurator_sets_up_opamp_without_deployment_environment_name(self, client_mock, agent_mock): + client_mock.return_value = client_mock + agent_mock.return_value = agent_mock + + ElasticOpenTelemetryConfigurator()._configure() + + client_mock.assert_called_once_with( + endpoint="https://localhost:4320/v1/opamp", + agent_identifying_attributes={"service.name": "service"}, + ) + agent_mock.assert_called_once_with(interval=30, message_handler=opamp_handler, client=client_mock) + agent_mock.start.assert_called_once_with() + + @mock.patch.dict("os.environ", {ELASTIC_OTEL_OPAMP_ENDPOINT: "localhost:4320/v1/opamp"}, clear=True) + @mock.patch("elasticotel.distro.OpAMPAgent") + @mock.patch("elasticotel.distro.OpAMPClient") + def test_configurator_ignores_invalid_url(self, client_mock, agent_mock): + with self.assertLogs(distro_logger, logging.WARNING): + ElasticOpenTelemetryConfigurator()._configure() + + client_mock.assert_not_called() + agent_mock.assert_not_called() + + @mock.patch.dict("os.environ", {ELASTIC_OTEL_OPAMP_ENDPOINT: "ws://localhost:4320/v1/opamp"}, clear=True) + @mock.patch("elasticotel.distro.OpAMPAgent") + @mock.patch("elasticotel.distro.OpAMPClient") + def test_configurator_ignores_ws_url(self, client_mock, agent_mock): + with self.assertLogs(distro_logger, logging.WARNING): + ElasticOpenTelemetryConfigurator()._configure() + client_mock.assert_not_called() + agent_mock.assert_not_called() + + @mock.patch.dict("os.environ", {}, clear=True) + @mock.patch("elasticotel.distro.OpAMPAgent") + @mock.patch("elasticotel.distro.OpAMPClient") + def test_configurator_ignores_opamp_without_endpoint(self, client_mock, agent_mock): + ElasticOpenTelemetryConfigurator()._configure() + client_mock.assert_not_called() + agent_mock.assert_not_called() + + +class TestOpAMPHandler(TestCase): + @mock.patch.object(logging, "getLogger") + def test_does_not_nothing_without_remote_config(self, get_logger_mock): + message = opamp_pb2.ServerToAgent() + client = mock.Mock() + opamp_handler(client, message) + + get_logger_mock.assert_not_called() + + @mock.patch.object(logging, "getLogger") + def test_ignores_non_elastic_filename(self, get_logger_mock): + client = mock.Mock() + config = opamp_pb2.AgentConfigMap() + config.config_map["non-elastic"].body = json.dumps({"logging_level": "trace"}).encode() + config.config_map["non-elastic"].content_type = "application/json" + remote_config = opamp_pb2.AgentRemoteConfig(config=config) + message = opamp_pb2.ServerToAgent(remote_config=remote_config) + opamp_handler(client, message) + + get_logger_mock.assert_not_called() + + @mock.patch.object(logging, "getLogger") + def test_sets_matching_logging_level(self, get_logger_mock): + client = mock.Mock() + config = opamp_pb2.AgentConfigMap() + config.config_map["elastic"].body = json.dumps({"logging_level": "trace"}).encode() + config.config_map["elastic"].content_type = "application/json" + remote_config = opamp_pb2.AgentRemoteConfig(config=config) + message = opamp_pb2.ServerToAgent(remote_config=remote_config) + opamp_handler(client, message) + + get_logger_mock.assert_has_calls( + [mock.call("opentelemetry"), mock.call().setLevel(5), mock.call("elasticotel"), mock.call().setLevel(5)] + ) + + @mock.patch.object(logging, "getLogger") + def test_sets_logging_to_default_info_without_logging_level_entry_in_config(self, get_logger_mock): + client = mock.Mock() + config = opamp_pb2.AgentConfigMap() + config.config_map["elastic"].body = json.dumps({}).encode() + config.config_map["elastic"].content_type = "application/json" + remote_config = opamp_pb2.AgentRemoteConfig(config=config) + message = opamp_pb2.ServerToAgent(remote_config=remote_config) + opamp_handler(client, message) + + get_logger_mock.assert_has_calls( + [ + mock.call("opentelemetry"), + mock.call().setLevel(logging.INFO), + mock.call("elasticotel"), + mock.call().setLevel(logging.INFO), + ] + ) + + @mock.patch.object(logging, "getLogger") + def test_warns_if_logging_level_does_not_match_our_map(self, get_logger_mock): + client = mock.Mock() + config = opamp_pb2.AgentConfigMap() + config.config_map["elastic"].body = json.dumps({"logging_level": "unexpected"}).encode() + config.config_map["elastic"].content_type = "application/json" + remote_config = opamp_pb2.AgentRemoteConfig(config=config) + message = opamp_pb2.ServerToAgent(remote_config=remote_config) + + with self.assertLogs(config_logger, logging.WARNING): + opamp_handler(client, message) diff --git a/tests/opamp/cassettes/test_connection_heartbeat_disconnection.yaml b/tests/opamp/cassettes/test_connection_heartbeat_disconnection.yaml new file mode 100644 index 00000000..8ceb2e4a --- /dev/null +++ b/tests/opamp/cassettes/test_connection_heartbeat_disconnection.yaml @@ -0,0 +1,103 @@ +interactions: +- request: + body: !!binary | + ChABl44aq8F1QqCWeJJQNmiKGj0KFQoMc2VydmljZS5uYW1lEgUKA2ZvbwokChtkZXBsb3ltZW50 + LmVudmlyb25tZW50Lm5hbWUSBQoDZm9vIINA + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '84' + Content-Type: + - application/x-protobuf + User-Agent: + - OTel-OpAMP-Python/0.0.1 + method: POST + uri: http://localhost:4320/v1/opamp + response: + body: + string: !!binary | + ChABl44aq8F1QqCWeJJQNmiKGmYKOgo4CgdlbGFzdGljEi0KGXsibG9nZ2luZ19sZXZlbCI6ImRl + YnVnIn0SEGFwcGxpY2F0aW9uL2pzb24SKGJmOThhY2M2NDNhMzJjNTcyZWFhYmVhNGIwZWU5ZTEw + NmFkYWRiZDY4AlIA + headers: + Content-Length: + - '126' + Content-Type: + - application/x-protobuf + Date: + - Fri, 20 Jun 2025 16:10:02 GMT + status: + code: 200 + message: OK +- request: + body: !!binary | + ChABl44aq8F1QqCWeJJQNmiKEAE= + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '20' + Content-Type: + - application/x-protobuf + User-Agent: + - OTel-OpAMP-Python/0.0.1 + method: POST + uri: http://localhost:4320/v1/opamp + response: + body: + string: !!binary | + ChABl44aq8F1QqCWeJJQNmiKGmYKOgo4CgdlbGFzdGljEi0KGXsibG9nZ2luZ19sZXZlbCI6ImRl + YnVnIn0SEGFwcGxpY2F0aW9uL2pzb24SKGJmOThhY2M2NDNhMzJjNTcyZWFhYmVhNGIwZWU5ZTEw + NmFkYWRiZDY4AlIA + headers: + Content-Length: + - '126' + Content-Type: + - application/x-protobuf + Date: + - Fri, 20 Jun 2025 16:10:03 GMT + status: + code: 200 + message: OK +- request: + body: !!binary | + ChABl44aq8F1QqCWeJJQNmiKEAJKAA== + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '22' + Content-Type: + - application/x-protobuf + User-Agent: + - OTel-OpAMP-Python/0.0.1 + method: POST + uri: http://localhost:4320/v1/opamp + response: + body: + string: !!binary | + ChABl44aq8F1QqCWeJJQNmiKOAJSAA== + headers: + Content-Length: + - '22' + Content-Type: + - application/x-protobuf + Date: + - Fri, 20 Jun 2025 16:10:03 GMT + status: + code: 200 + message: OK +version: 1 diff --git a/tests/opamp/cassettes/test_with_server_not_responding.yaml b/tests/opamp/cassettes/test_with_server_not_responding.yaml new file mode 100644 index 00000000..729aacc0 --- /dev/null +++ b/tests/opamp/cassettes/test_with_server_not_responding.yaml @@ -0,0 +1,37 @@ +interactions: +- request: + body: !!binary | + ChABl5u4Znl1co2DffBeGpZwGj0KFQoMc2VydmljZS5uYW1lEgUKA2ZvbwokChtkZXBsb3ltZW50 + LmVudmlyb25tZW50Lm5hbWUSBQoDZm9vIINA + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '84' + Content-Type: + - application/x-protobuf + User-Agent: + - OTel-OpAMP-Python/0.0.1 + method: POST + uri: http://localhost:4320/v1/opamp + response: + body: + string: !!binary | + ChABl5u4Znl1co2DffBeGpZwGmYKOgo4CgdlbGFzdGljEi0KGXsibG9nZ2luZ19sZXZlbCI6ImRl + YnVnIn0SEGFwcGxpY2F0aW9uL2pzb24SKGJmOThhY2M2NDNhMzJjNTcyZWFhYmVhNGIwZWU5ZTEw + NmFkYWRiZDY4AlIA + headers: + Content-Length: + - '126' + Content-Type: + - application/x-protobuf + Date: + - Mon, 23 Jun 2025 07:37:22 GMT + status: + code: 200 + message: OK +version: 1 diff --git a/tests/opamp/conftest.py b/tests/opamp/conftest.py new file mode 100644 index 00000000..89516435 --- /dev/null +++ b/tests/opamp/conftest.py @@ -0,0 +1,35 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + + +@pytest.fixture(scope="module") +def vcr_config(): + return { + "filter_headers": [ + ("authorization", "Bearer key"), + ], + "decode_compressed_response": True, + "before_record_response": scrub_response_headers, + } + + +def scrub_response_headers(response): + """ + This scrubs sensitive response headers. Note they are case-sensitive! + """ + return response diff --git a/tests/opamp/test_agent.py b/tests/opamp/test_agent.py new file mode 100644 index 00000000..5a498c1a --- /dev/null +++ b/tests/opamp/test_agent.py @@ -0,0 +1,168 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from time import sleep +from unittest import mock + +from opentelemetry._opamp.agent import _Job as Job, OpAMPAgent + + +def test_can_instantiate_agent(): + agent = OpAMPAgent(interval=30, client=mock.Mock(), message_handler=mock.Mock()) + assert isinstance(agent, OpAMPAgent) + + +def test_can_start_agent(): + agent = OpAMPAgent(interval=30, client=mock.Mock(), message_handler=mock.Mock()) + agent.start() + agent.stop() + + +def test_agent_start_will_send_connection_and_disconnetion_messages(): + client_mock = mock.Mock() + mock_message = {"mock": "message"} + client_mock._send.return_value = mock_message + message_handler = mock.Mock() + agent = OpAMPAgent(interval=30, client=client_mock, message_handler=message_handler) + agent.start() + # wait for the queue to be consumed + sleep(0.1) + agent.stop() + + # one send for connection message, one for disconnect agent message + assert client_mock._send.call_count == 2 + # connection callback has been called + assert agent._schedule is True + # connection message response has been received + message_handler.assert_called_once_with(client_mock, mock_message) + + +def test_agent_can_call_agent_stop_multiple_times(): + agent = OpAMPAgent(interval=30, client=mock.Mock(), message_handler=mock.Mock()) + agent.start() + agent.stop() + agent.stop() + + +def test_agent_can_call_agent_stop_before_start(): + agent = OpAMPAgent(interval=30, client=mock.Mock(), message_handler=mock.Mock()) + agent.stop() + + +def test_agent_send_warns_without_worker_thread(caplog): + agent = OpAMPAgent(interval=30, client=mock.Mock(), message_handler=mock.Mock()) + agent.send(payload="payload") + + assert caplog.record_tuples == [ + ( + "opentelemetry._opamp.agent", + logging.WARNING, + "Called send() but worker thread is not alive. Worker threads is started with start()", + ) + ] + + +def test_agent_retries_before_max_attempts(caplog): + caplog.set_level(logging.DEBUG, logger="opentelemetry._opamp.agent") + message_handler_mock = mock.Mock() + client_mock = mock.Mock() + connection_message = disconnection_message = server_message = mock.Mock() + client_mock._send.side_effect = [connection_message, Exception, server_message, disconnection_message] + agent = OpAMPAgent(interval=30, client=client_mock, message_handler=message_handler_mock, initial_backoff=0) + agent.start() + agent.send(payload="payload") + # wait for the queue to be consumed + sleep(0.1) + agent.stop() + + assert client_mock._send.call_count == 4 + assert message_handler_mock.call_count == 2 + + +def test_agent_stops_after_max_attempts(caplog): + caplog.set_level(logging.DEBUG, logger="opentelemetry._opamp.agent") + message_handler_mock = mock.Mock() + client_mock = mock.Mock() + connection_message = disconnection_message = mock.Mock() + client_mock._send.side_effect = [connection_message, Exception, Exception, disconnection_message] + agent = OpAMPAgent( + interval=30, client=client_mock, message_handler=message_handler_mock, max_retries=1, initial_backoff=0 + ) + agent.start() + agent.send(payload="payload") + # wait for the queue to be consumed + sleep(0.1) + agent.stop() + + assert client_mock._send.call_count == 4 + assert message_handler_mock.call_count == 1 + + +def test_agent_send_enqueues_job(): + message_handler_mock = mock.Mock() + agent = OpAMPAgent(interval=30, client=mock.Mock(), message_handler=message_handler_mock) + agent.start() + # wait for the queue to be consumed + sleep(0.1) + # message handler called for connection message + assert message_handler_mock.call_count == 1 + agent.send(payload="payload") + # wait for the queue to be consumed + sleep(0.1) + agent.stop() + + # message handler called once for connection and once for our message + assert message_handler_mock.call_count == 2 + + +def test_can_instantiate_job(): + job = Job(payload="payload") + + assert isinstance(job, Job) + + +def test_job_should_retry(): + job = Job(payload="payload") + assert job.attempt == 0 + assert job.max_retries == 1 + assert job.should_retry() is True + + job.attempt += 1 + assert job.should_retry() is True + + job.attempt += 1 + assert job.should_retry() is False + + +def test_job_delay(): + job = Job(payload="payload") + + assert job.initial_backoff == 1 + job.attempt = 1 + assert job.initial_backoff * 0.8 <= job.delay() <= job.initial_backoff * 1.2 + + job.attempt = 2 + assert 2 * job.initial_backoff * 0.8 <= job.delay() <= 2 * job.initial_backoff * 1.2 + + job.attempt = 3 + assert (2**2) * job.initial_backoff * 0.8 <= job.delay() <= (2**2) * job.initial_backoff * 1.2 + + +def test_job_delay_has_jitter(): + job = Job(payload="payload") + job.attempt = 1 + assert len(set([job.delay() for i in range(10)])) > 1 diff --git a/tests/opamp/test_client.py b/tests/opamp/test_client.py new file mode 100644 index 00000000..d61821a3 --- /dev/null +++ b/tests/opamp/test_client.py @@ -0,0 +1,212 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from unittest import mock + +import pytest + +from opentelemetry._opamp.client import OpAMPClient, _HANDLED_CAPABILITIES +from opentelemetry._opamp.exceptions import OpAMPRemoteConfigDecodeException, OpAMPRemoteConfigParseException +from opentelemetry._opamp.proto import opamp_pb2 +from opentelemetry._opamp.proto.anyvalue_pb2 import KeyValue as PB2KeyValue, AnyValue as PB2AnyValue +from opentelemetry._opamp.version import __version__ + + +@pytest.fixture +def client(): + return OpAMPClient(endpoint="url", agent_identifying_attributes={"foo": "bar"}) + + +def test_can_instantiate_opamp_client_with_defaults(): + client = OpAMPClient(endpoint="url", agent_identifying_attributes={"foo": "bar"}) + + assert client + assert client._headers == { + "Content-Type": "application/x-protobuf", + "User-Agent": "OTel-OpAMP-Python/" + __version__, + } + assert client._timeout_millis == 1_000 + assert client._sequence_num == 0 + assert isinstance(client._instance_uid, bytes) + assert isinstance(client._agent_description, opamp_pb2.AgentDescription) + assert client._agent_description.identifying_attributes == [ + PB2KeyValue(key="foo", value=PB2AnyValue(string_value="bar")), + ] + assert client._agent_description.non_identifying_attributes == [] + + +def test_can_instantiate_opamp_client_all_params(): + client = OpAMPClient( + endpoint="url", + headers={"an": "header"}, + timeout_millis=2_000, + agent_identifying_attributes={"foo": "bar"}, + agent_non_identifying_attributes={"bar": "baz"}, + ) + + assert client + assert client._headers == { + "Content-Type": "application/x-protobuf", + "User-Agent": "OTel-OpAMP-Python/" + __version__, + "an": "header", + } + assert client._timeout_millis == 2_000 + assert client._sequence_num == 0 + assert isinstance(client._instance_uid, bytes) + assert isinstance(client._agent_description, opamp_pb2.AgentDescription) + assert client._agent_description.identifying_attributes == [ + PB2KeyValue(key="foo", value=PB2AnyValue(string_value="bar")), + ] + assert client._agent_description.non_identifying_attributes == [ + PB2KeyValue(key="bar", value=PB2AnyValue(string_value="baz")), + ] + + +def test_client_headers_override_defaults(): + client = OpAMPClient(endpoint="url", agent_identifying_attributes={"foo": "bar"}, headers={"User-Agent": "Custom"}) + client._transport = mock.Mock() + client._send(b"") + + (send_call,) = client._transport.mock_calls + assert send_call == mock.call.send( + url="url", + headers={"Content-Type": "application/x-protobuf", "User-Agent": "Custom"}, + data=b"", + timeout_millis=1000, + ) + + +def test_build_connection_message(client): + data = client._build_connection_message() + + message = opamp_pb2.AgentToServer() + message.ParseFromString(data) + + assert message + assert message.instance_uid == client._instance_uid + assert message.sequence_num == 0 + assert message.agent_description.identifying_attributes == [ + PB2KeyValue(key="foo", value=PB2AnyValue(string_value="bar")), + ] + assert message.agent_description.non_identifying_attributes == [] + assert message.capabilities == _HANDLED_CAPABILITIES + + +def test_build_connection_message_can_serialize_attributes(): + client = OpAMPClient( + endpoint="url", + agent_identifying_attributes={"string": "s", "bytes": b"b", "none": None, "bool": True, "int": 1, "float": 2.0}, + ) + data = client._build_connection_message() + + message = opamp_pb2.AgentToServer() + message.ParseFromString(data) + + assert message + assert message.instance_uid == client._instance_uid + assert message.sequence_num == 0 + assert message.agent_description.identifying_attributes == [ + PB2KeyValue(key="string", value=PB2AnyValue(string_value="s")), + PB2KeyValue(key="bytes", value=PB2AnyValue(bytes_value=b"b")), + PB2KeyValue(key="none", value=PB2AnyValue()), + PB2KeyValue(key="bool", value=PB2AnyValue(bool_value=True)), + PB2KeyValue(key="int", value=PB2AnyValue(int_value=1)), + PB2KeyValue(key="float", value=PB2AnyValue(double_value=2.0)), + ] + assert message.agent_description.non_identifying_attributes == [] + assert message.capabilities == _HANDLED_CAPABILITIES + + +def test_build_agent_disconnect_message(client): + data = client._build_agent_disconnect_message() + + message = opamp_pb2.AgentToServer() + message.ParseFromString(data) + + assert message + assert message.instance_uid == client._instance_uid + assert message.sequence_num == 0 + assert message.agent_disconnect == opamp_pb2.AgentDisconnect() + + +def test_build_heartbeat_message(client): + data = client._build_heartbeat_message() + + message = opamp_pb2.AgentToServer() + message.ParseFromString(data) + + assert message + assert message.instance_uid == client._instance_uid + assert message.sequence_num == 0 + + +def test_message_sequence_num_increases_in_send(client): + client._transport = mock.Mock() + for i in range(2): + data = client._build_heartbeat_message() + client._send(data) + + message = opamp_pb2.AgentToServer() + message.ParseFromString(data) + + assert message + assert message.sequence_num == i + + +def test_send(client): + client._transport = mock.Mock() + client._send(b"foo") + + (send_call,) = client._transport.mock_calls + assert send_call == mock.call.send( + url="url", + headers={"Content-Type": "application/x-protobuf", "User-Agent": "OTel-OpAMP-Python/" + __version__}, + data=b"foo", + timeout_millis=1000, + ) + + +def test_decode_remote_config(client): + config = opamp_pb2.AgentConfigMap() + config.config_map["application/json"].body = json.dumps({"a": "config"}).encode() + config.config_map["application/json"].content_type = "application/json" + config.config_map["text/json"].body = json.dumps({"other": "config"}).encode() + config.config_map["text/json"].content_type = "text/json" + message = opamp_pb2.AgentRemoteConfig(config=config) + + decoded = [(filename, config) for filename, config in client._decode_remote_config(message)] + assert sorted(decoded) == sorted([("application/json", {"a": "config"}), ("text/json", {"other": "config"})]) + + +def test_decode_remote_config_invalid_content_type(client): + config = opamp_pb2.AgentConfigMap() + config.config_map["filename"].body = b"1" + config.config_map["filename"].content_type = "not/json" + message = opamp_pb2.AgentRemoteConfig(config=config) + + with pytest.raises(OpAMPRemoteConfigParseException): + list(client._decode_remote_config(message)) + + +def test_decode_remote_config_invalid_file_body(client): + config = opamp_pb2.AgentConfigMap() + config.config_map["filename"].body = b"notjson" + config.config_map["filename"].content_type = "application/json" + message = opamp_pb2.AgentRemoteConfig(config=config) + + with pytest.raises(OpAMPRemoteConfigDecodeException): + list(client._decode_remote_config(message)) diff --git a/tests/opamp/test_e2e.py b/tests/opamp/test_e2e.py new file mode 100644 index 00000000..5c352392 --- /dev/null +++ b/tests/opamp/test_e2e.py @@ -0,0 +1,81 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import sys +from time import sleep +from unittest import mock + +import pytest + +from opentelemetry._opamp.agent import OpAMPAgent +from opentelemetry._opamp.client import OpAMPClient + + +@pytest.mark.skipif(sys.version_info < (3, 10), reason="vcr.py not working with urllib 2 and older Pythons") +@pytest.mark.vcr() +def test_connection_heartbeat_disconnection(caplog): + caplog.set_level(logging.DEBUG, logger="opentelemetry._opamp.agent") + + opamp_handler = mock.Mock() + + opamp_client = OpAMPClient( + endpoint="http://localhost:4320/v1/opamp", + agent_identifying_attributes={ + "service.name": "foo", + "deployment.environment.name": "foo", + }, + ) + opamp_agent = OpAMPAgent( + interval=1, + message_handler=opamp_handler, + client=opamp_client, + ) + opamp_agent.start() + + # this should be enough for the heartbeat message to be sent + sleep(1.5) + + opamp_agent.stop() + + # one call is for connection and the other one is heartbeat + assert opamp_handler.call_count == 2 + + +@pytest.mark.skipif(sys.version_info < (3, 10), reason="vcr.py not working with urllib 2 and older Pythons") +@pytest.mark.vcr() +def test_with_server_not_responding(caplog): + caplog.set_level(logging.DEBUG, logger="opentelemetry._opamp.agent") + + opamp_handler = mock.Mock() + + opamp_client = OpAMPClient( + endpoint="http://localhost:4321/v1/opamp", + agent_identifying_attributes={ + "service.name": "foo", + "deployment.environment.name": "foo", + }, + ) + opamp_agent = OpAMPAgent( + interval=1, + message_handler=opamp_handler, + client=opamp_client, + ) + opamp_agent.start() + + opamp_agent.stop() + + assert opamp_handler.call_count == 0 diff --git a/tests/opamp/transport/test_requests.py b/tests/opamp/transport/test_requests.py new file mode 100644 index 00000000..bc7e4f40 --- /dev/null +++ b/tests/opamp/transport/test_requests.py @@ -0,0 +1,65 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import mock + +import pytest + +from opentelemetry._opamp.proto import opamp_pb2 +from opentelemetry._opamp.transport.base import base_headers +from opentelemetry._opamp.transport.exceptions import OpAMPException +from opentelemetry._opamp.transport.requests import RequestsTransport + + +def test_can_instantiate_requests_transport(): + transport = RequestsTransport() + + assert transport + + +def test_can_send(): + transport = RequestsTransport() + serialized_message = opamp_pb2.ServerToAgent().SerializeToString() + response_mock = mock.Mock(content=serialized_message) + headers = {"foo": "bar"} + expected_headers = {**base_headers, **headers} + data = b"" + with mock.patch.object(transport, "session") as session_mock: + session_mock.post.return_value = response_mock + response = transport.send("http://127.0.0.1/v1/opamp", headers=headers, data=data, timeout_millis=1_000) + + session_mock.post.assert_called_once_with( + "http://127.0.0.1/v1/opamp", headers=expected_headers, data=data, timeout=1 + ) + + assert isinstance(response, opamp_pb2.ServerToAgent) + + +def test_send_exceptions_raises_opamp_exception(): + transport = RequestsTransport() + response_mock = mock.Mock() + headers = {"foo": "bar"} + expected_headers = {**base_headers, **headers} + data = b"" + with mock.patch.object(transport, "session") as session_mock: + session_mock.post.return_value = response_mock + response_mock.raise_for_status.side_effect = Exception + with pytest.raises(OpAMPException): + transport.send("http://127.0.0.1/v1/opamp", headers=headers, data=data, timeout_millis=1_000) + + session_mock.post.assert_called_once_with( + "http://127.0.0.1/v1/opamp", headers=expected_headers, data=data, timeout=1 + )