From cf61c5732f4bb4442794dd947af9ef0d09cdaf21 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 11 Jul 2025 16:45:38 +0200 Subject: [PATCH 01/39] Add a basic http OpAMP client --- .pylintrc | 2 +- opamp-gen-requirements.txt | 5 + opamp/opentelemetry-opamp-client/LICENSE | 201 ++ opamp/opentelemetry-opamp-client/README.rst | 22 + .../opentelemetry-opamp-client/pyproject.toml | 45 + .../src/opentelemetry/_opamp/__init__.py | 0 .../src/opentelemetry/_opamp/agent.py | 276 +++ .../src/opentelemetry/_opamp/client.py | 159 ++ .../src/opentelemetry/_opamp/exceptions.py | 25 + .../src/opentelemetry/_opamp/messages.py | 165 ++ .../_opamp/proto/anyvalue_pb2.py | 33 + .../_opamp/proto/anyvalue_pb2.pyi | 135 ++ .../opentelemetry/_opamp/proto/opamp_pb2.py | 144 ++ .../opentelemetry/_opamp/proto/opamp_pb2.pyi | 1987 +++++++++++++++++ .../opentelemetry/_opamp/transport/base.py | 34 + .../_opamp/transport/exceptions.py | 17 + .../_opamp/transport/requests.py | 47 + .../src/opentelemetry/_opamp/version.py | 15 + .../test-requirements.in | 15 + .../test-requirements.latest.txt | 78 + .../test-requirements.lowest.txt | 74 + ...config_status_heartbeat_disconnection.yaml | 134 ++ .../tests/opamp/conftest.py | 33 + .../tests/opamp/test_agent.py | 210 ++ .../tests/opamp/test_client.py | 365 +++ .../tests/opamp/test_e2e.py | 115 + .../tests/opamp/transport/test_requests.py | 79 + scripts/opamp_proto_codegen.sh | 81 + tox.ini | 15 + 29 files changed, 4510 insertions(+), 1 deletion(-) create mode 100644 opamp-gen-requirements.txt create mode 100644 opamp/opentelemetry-opamp-client/LICENSE create mode 100644 opamp/opentelemetry-opamp-client/README.rst create mode 100644 opamp/opentelemetry-opamp-client/pyproject.toml create mode 100644 opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py create mode 100644 opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py create mode 100644 opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py create mode 100644 opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/exceptions.py create mode 100644 opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py create mode 100644 opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/proto/anyvalue_pb2.py create mode 100644 opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/proto/anyvalue_pb2.pyi create mode 100644 opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/proto/opamp_pb2.py create mode 100644 opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/proto/opamp_pb2.pyi create mode 100644 opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/base.py create mode 100644 opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/exceptions.py create mode 100644 opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/requests.py create mode 100644 opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/version.py create mode 100644 opamp/opentelemetry-opamp-client/test-requirements.in create mode 100644 opamp/opentelemetry-opamp-client/test-requirements.latest.txt create mode 100644 opamp/opentelemetry-opamp-client/test-requirements.lowest.txt create mode 100644 opamp/opentelemetry-opamp-client/tests/opamp/cassettes/test_connection_remote_config_status_heartbeat_disconnection.yaml create mode 100644 opamp/opentelemetry-opamp-client/tests/opamp/conftest.py create mode 100644 opamp/opentelemetry-opamp-client/tests/opamp/test_agent.py create mode 100644 opamp/opentelemetry-opamp-client/tests/opamp/test_client.py create mode 100644 opamp/opentelemetry-opamp-client/tests/opamp/test_e2e.py create mode 100644 opamp/opentelemetry-opamp-client/tests/opamp/transport/test_requests.py create mode 100755 scripts/opamp_proto_codegen.sh diff --git a/.pylintrc b/.pylintrc index e51f6f43bd..496e1d846b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -7,7 +7,7 @@ extension-pkg-whitelist=cassandra # Add list of files or directories to be excluded. They should be base names, not # paths. -ignore=CVS,gen,Dockerfile,docker-compose.yml,README.md,requirements.txt,docs,.venv,site-packages,.tox +ignore=CVS,gen,Dockerfile,docker-compose.yml,README.md,requirements.txt,docs,.venv,site-packages,.tox,proto # Add files or directories matching the regex patterns to be excluded. The # regex matches against base names, not paths. diff --git a/opamp-gen-requirements.txt b/opamp-gen-requirements.txt new file mode 100644 index 0000000000..3cd7e79a44 --- /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/opamp/opentelemetry-opamp-client/LICENSE b/opamp/opentelemetry-opamp-client/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/opamp/opentelemetry-opamp-client/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/opamp/opentelemetry-opamp-client/README.rst b/opamp/opentelemetry-opamp-client/README.rst new file mode 100644 index 0000000000..9984fc90d0 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/README.rst @@ -0,0 +1,22 @@ +OpenTelemetry OpAMP Client +========================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-opamp-client.svg + :target: https://pypi.org/project/opentelemetry-opamp-client/ + +Installation +------------ + +:: + + pip install opentelemetry-opamp-client + + +References +---------- +* `OpenTelemetry OpAMP Client `_ +* `OpenTelemetry Project `_ +* `OpenTelemetry Python Examples `_ + diff --git a/opamp/opentelemetry-opamp-client/pyproject.toml b/opamp/opentelemetry-opamp-client/pyproject.toml new file mode 100644 index 0000000000..67db850890 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/pyproject.toml @@ -0,0 +1,45 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "opentelemetry-opamp-client" +dynamic = ["version"] +description = "OpenTelemetry OpAMP client" +readme = "README.rst" +license = "Apache-2.0" +requires-python = ">=3.9" +authors = [ + { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "protobuf>=5.0, < 7.0", +] + +[project.urls] +Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/opamp/opentelemetry-opamp-client" +Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib" + +[tool.hatch.version] +path = "src/opentelemetry/_opamp/version.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/opentelemetry"] diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py new file mode 100644 index 0000000000..a81f94a870 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py @@ -0,0 +1,276 @@ +# 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. + +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 + +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[ + ["OpAMPAgent", 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. + """ + # pylint: disable=broad-exception-caught + 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.debug("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 + + if message is not None: + # we can't do much if the handler fails other than logging + try: + self._handler(self, 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: # pylint: disable=broad-exception-caught + 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/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py new file mode 100644 index 0000000000..deaccead84 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py @@ -0,0 +1,159 @@ +# 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. + +from __future__ import annotations + +from logging import getLogger +from typing import Generator, Mapping + +from uuid_utils import uuid7 + +from opentelemetry._opamp import messages +from opentelemetry._opamp.proto import opamp_pb2 +from opentelemetry._opamp.transport.requests import RequestsTransport +from opentelemetry._opamp.version import __version__ +from opentelemetry.util.types import AnyValue + +_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 + | opamp_pb2.AgentCapabilities.AgentCapabilities_ReportsRemoteConfig +) + + +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 + self._remote_config_status: opamp_pb2.RemoteConfigStatus | None = None + + 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 _update_remote_config_status( + self, + remote_config_hash: bytes, + status: opamp_pb2.RemoteConfigStatuses.ValueType, + error_message: str = "", + ) -> opamp_pb2.RemoteConfigStatus | None: + status_changed = ( + not self._remote_config_status + or self._remote_config_status.last_remote_config_hash + != remote_config_hash + or self._remote_config_status.status != status + or self._remote_config_status.error_message != error_message + ) + # if the status changed update we return the RemoteConfigStatus message so that we can send it to the server + if status_changed: + _logger.debug( + "Update remote config status changed for %s", + remote_config_hash, + ) + self._remote_config_status = ( + messages._build_remote_config_status_message( + last_remote_config_hash=remote_config_hash, + status=status, + error_message=error_message, + ) + ) + return self._remote_config_status + + return None + + def _build_remote_config_status_response_message( + self, remote_config_status: opamp_pb2.RemoteConfigStatus + ) -> bytes: + message = messages._build_remote_config_status_response_message( + instance_uid=self._instance_uid, + sequence_num=self._sequence_num, + capabilities=_HANDLED_CAPABILITIES, + remote_config_status=remote_config_status, + ) + 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 + finally: + self._sequence_num += 1 + + @staticmethod + def _decode_remote_config( + 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/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/exceptions.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/exceptions.py new file mode 100644 index 0000000000..2f573b0a55 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/exceptions.py @@ -0,0 +1,25 @@ +# 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. + + +class OpAMPTimeoutError(Exception): + pass + + +class OpAMPRemoteConfigParseException(Exception): + pass + + +class OpAMPRemoteConfigDecodeException(Exception): + pass diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py new file mode 100644 index 0000000000..f9410d4a11 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py @@ -0,0 +1,165 @@ +# 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. + +from __future__ import annotations + +import json +from typing import Generator, Mapping + +from opentelemetry._opamp.exceptions import ( + OpAMPRemoteConfigDecodeException, + OpAMPRemoteConfigParseException, +) +from opentelemetry._opamp.proto import opamp_pb2 +from opentelemetry._opamp.proto.anyvalue_pb2 import AnyValue as PB2AnyValue +from opentelemetry._opamp.proto.anyvalue_pb2 import KeyValue as PB2KeyValue +from opentelemetry.util.types import AnyValue + + +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 _build_remote_config_status_message( + last_remote_config_hash: bytes, + status: opamp_pb2.RemoteConfigStatuses.ValueType, + error_message: str = "", +) -> opamp_pb2.RemoteConfigStatus: + return opamp_pb2.RemoteConfigStatus( + last_remote_config_hash=last_remote_config_hash, + status=status, + error_message=error_message, + ) + + +def _build_remote_config_status_response_message( + instance_uid: bytes, + sequence_num: int, + capabilities: int, + remote_config_status: opamp_pb2.RemoteConfigStatus, +) -> opamp_pb2.AgentToServer: + command = opamp_pb2.AgentToServer( + instance_uid=instance_uid, + sequence_num=sequence_num, + remote_config_status=remote_config_status, + 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}" + ) + + 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/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/proto/anyvalue_pb2.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/proto/anyvalue_pb2.py new file mode 100644 index 0000000000..7d1cd9b5b6 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/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/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/proto/anyvalue_pb2.pyi b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/proto/anyvalue_pb2.pyi new file mode 100644 index 0000000000..5036b8eb5a --- /dev/null +++ b/opamp/opentelemetry-opamp-client/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/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/proto/opamp_pb2.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/proto/opamp_pb2.py new file mode 100644 index 0000000000..00a62682d6 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/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/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/proto/opamp_pb2.pyi b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/proto/opamp_pb2.pyi new file mode 100644 index 0000000000..1f0ad4a217 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/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/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/base.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/base.py new file mode 100644 index 0000000000..66ce6fd429 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/base.py @@ -0,0 +1,34 @@ +# 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. + +import abc +from typing import Mapping + +from opentelemetry._opamp.proto import 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/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/exceptions.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/exceptions.py new file mode 100644 index 0000000000..e57aeb53f7 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/exceptions.py @@ -0,0 +1,17 @@ +# 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. + + +class OpAMPException(Exception): + pass diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/requests.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/requests.py new file mode 100644 index 0000000000..6ce2c0c775 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/requests.py @@ -0,0 +1,47 @@ +# 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. + +from typing import Mapping + +import requests + +from opentelemetry._opamp import messages +from opentelemetry._opamp.transport.base import HttpTransport, base_headers +from opentelemetry._opamp.transport.exceptions import OpAMPException + + +class RequestsTransport(HttpTransport): + def __init__(self): + self.session = requests.Session() + + 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/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/version.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/version.py new file mode 100644 index 0000000000..d556297f28 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/version.py @@ -0,0 +1,15 @@ +# 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. + +__version__ = "0.57b0.dev" diff --git a/opamp/opentelemetry-opamp-client/test-requirements.in b/opamp/opentelemetry-opamp-client/test-requirements.in new file mode 100644 index 0000000000..12929cc350 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/test-requirements.in @@ -0,0 +1,15 @@ +colorama>=0.4.6 +iniconfig>=2.0.0 +packaging>=24.0 +pluggy>=1.5.0 +protobuf>=5.29.5 +pytest>=7.4.4 +pytest-vcr>=1.0.2 ; python_version > '3.9' and platform_python_implementation !='PyPy' +pyyaml>=6.0.2 +vcrpy>=6.0.2 ; python_version > '3.9' and platform_python_implementation !='PyPy' +yarl>=1.20.1 +requests>=2.32.2 +urllib3>=2.5.0 +uuid-utils>=0.11.0 +wrapt>=1.16.0 +-e opamp/opentelemetry-opamp-client diff --git a/opamp/opentelemetry-opamp-client/test-requirements.latest.txt b/opamp/opentelemetry-opamp-client/test-requirements.latest.txt new file mode 100644 index 0000000000..8e730e144f --- /dev/null +++ b/opamp/opentelemetry-opamp-client/test-requirements.latest.txt @@ -0,0 +1,78 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --python 3.9 --universal -c dev-requirements.txt opamp/opentelemetry-opamp-client/test-requirements.in -o opamp/opentelemetry-opamp-client/test-requirements.latest.txt +-e opamp/opentelemetry-opamp-client + # via -r opamp/opentelemetry-opamp-client/test-requirements.in +certifi==2025.7.9 + # via requests +charset-normalizer==3.4.2 + # via requests +colorama==0.4.6 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # pytest +exceptiongroup==1.3.0 ; python_full_version < '3.11' + # via pytest +idna==3.10 + # via + # requests + # yarl +iniconfig==2.1.0 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # pytest +multidict==6.6.3 + # via yarl +packaging==25.0 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # pytest +pluggy==1.6.0 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # pytest +propcache==0.3.2 + # via yarl +protobuf==6.31.1 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # opentelemetry-opamp-client +pytest==7.4.4 + # via + # -c dev-requirements.txt + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # pytest-vcr +pytest-vcr==1.0.2 ; python_full_version >= '3.10' and platform_python_implementation != 'PyPy' + # via -r opamp/opentelemetry-opamp-client/test-requirements.in +pyyaml==6.0.2 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # vcrpy +requests==2.32.3 + # via + # -c dev-requirements.txt + # -r opamp/opentelemetry-opamp-client/test-requirements.in +tomli==2.2.1 ; python_full_version < '3.11' + # via pytest +typing-extensions==4.14.0 ; python_full_version < '3.11' + # via + # exceptiongroup + # multidict +urllib3==2.5.0 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # requests + # vcrpy +uuid-utils==0.11.0 + # via -r opamp/opentelemetry-opamp-client/test-requirements.in +vcrpy==7.0.0 ; python_full_version >= '3.10' and platform_python_implementation != 'PyPy' + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # pytest-vcr +wrapt==1.17.2 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # vcrpy +yarl==1.20.1 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # vcrpy diff --git a/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt b/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt new file mode 100644 index 0000000000..5669ed119d --- /dev/null +++ b/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt @@ -0,0 +1,74 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --python 3.9 --universal --resolution lowest -c dev-requirements.txt opamp/opentelemetry-opamp-client/test-requirements.in -o opamp/opentelemetry-opamp-client/test-requirements.lowest.txt +-e opamp/opentelemetry-opamp-client + # via -r opamp/opentelemetry-opamp-client/test-requirements.in +certifi==2017.4.17 + # via requests +charset-normalizer==2.0.0 + # via requests +colorama==0.4.6 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # pytest +exceptiongroup==1.0.0 ; python_full_version < '3.11' + # via pytest +idna==2.5 + # via + # requests + # yarl +iniconfig==2.0.0 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # pytest +multidict==4.0.0 + # via yarl +packaging==24.0 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # pytest +pluggy==1.5.0 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # pytest +propcache==0.2.1 + # via yarl +protobuf==5.29.5 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # opentelemetry-opamp-client +pytest==7.4.4 + # via + # -c dev-requirements.txt + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # pytest-vcr +pytest-vcr==1.0.2 ; python_full_version >= '3.10' and platform_python_implementation != 'PyPy' + # via -r opamp/opentelemetry-opamp-client/test-requirements.in +pyyaml==6.0.2 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # vcrpy +requests==2.32.3 + # via + # -c dev-requirements.txt + # -r opamp/opentelemetry-opamp-client/test-requirements.in +tomli==1.0.0 ; python_full_version < '3.11' + # via pytest +urllib3==2.5.0 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # requests + # vcrpy +uuid-utils==0.11.0 + # via -r opamp/opentelemetry-opamp-client/test-requirements.in +vcrpy==6.0.2 ; python_full_version >= '3.10' and platform_python_implementation != 'PyPy' + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # pytest-vcr +wrapt==1.16.0 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # vcrpy +yarl==1.20.1 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # vcrpy diff --git a/opamp/opentelemetry-opamp-client/tests/opamp/cassettes/test_connection_remote_config_status_heartbeat_disconnection.yaml b/opamp/opentelemetry-opamp-client/tests/opamp/cassettes/test_connection_remote_config_status_heartbeat_disconnection.yaml new file mode 100644 index 0000000000..7497aaba48 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/tests/opamp/cassettes/test_connection_remote_config_status_heartbeat_disconnection.yaml @@ -0,0 +1,134 @@ +interactions: +- request: + body: !!binary | + ChABl89ktxVxE7d60nbzJJzzGj0KFQoMc2VydmljZS5uYW1lEgUKA2ZvbwokChtkZXBsb3ltZW50 + LmVudmlyb25tZW50Lm5hbWUSBQoDZm9vIINg + 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 | + ChABl89ktxVxE7d60nbzJJzzGmYKOgo4CgdlbGFzdGljEi0KGXsibG9nZ2luZ19sZXZlbCI6ImRl + YnVnIn0SEGFwcGxpY2F0aW9uL2pzb24SKGY3MzA5M2VjZDEyNjkzZGMxNDUxYWQ2MjdlZDA2MWJl + ZWM5ZjU1OWM4AlIA + headers: + Content-Length: + - '126' + Content-Type: + - application/x-protobuf + Date: + - Thu, 03 Jul 2025 08:26:13 GMT + status: + code: 200 + message: OK +- request: + body: !!binary | + ChABl89ktxVxE7d60nbzJJzzEAEgg2A6LAooZjczMDkzZWNkMTI2OTNkYzE0NTFhZDYyN2VkMDYx + YmVlYzlmNTU5YxAB + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '69' + 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 | + ChABl89ktxVxE7d60nbzJJzzOAJSAA== + headers: + Content-Length: + - '22' + Content-Type: + - application/x-protobuf + Date: + - Thu, 03 Jul 2025 08:26:13 GMT + status: + code: 200 + message: OK +- request: + body: !!binary | + ChABl89ktxVxE7d60nbzJJzzEAIgg2A= + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '23' + 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 | + ChABl89ktxVxE7d60nbzJJzzOAJSAA== + headers: + Content-Length: + - '22' + Content-Type: + - application/x-protobuf + Date: + - Thu, 03 Jul 2025 08:26:14 GMT + status: + code: 200 + message: OK +- request: + body: !!binary | + ChABl89ktxVxE7d60nbzJJzzEAMgg2BKAA== + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '25' + 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 | + ChABl89ktxVxE7d60nbzJJzzOAJSAA== + headers: + Content-Length: + - '22' + Content-Type: + - application/x-protobuf + Date: + - Thu, 03 Jul 2025 08:26:15 GMT + status: + code: 200 + message: OK +version: 1 diff --git a/opamp/opentelemetry-opamp-client/tests/opamp/conftest.py b/opamp/opentelemetry-opamp-client/tests/opamp/conftest.py new file mode 100644 index 0000000000..790f97f8e5 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/tests/opamp/conftest.py @@ -0,0 +1,33 @@ +# 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. + +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/opamp/opentelemetry-opamp-client/tests/opamp/test_agent.py b/opamp/opentelemetry-opamp-client/tests/opamp/test_agent.py new file mode 100644 index 0000000000..dff25b24c2 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/tests/opamp/test_agent.py @@ -0,0 +1,210 @@ +# 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. + +import logging +from time import sleep +from unittest import mock + +from opentelemetry._opamp.agent import OpAMPAgent +from opentelemetry._opamp.agent import _Job as Job + + +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(agent, 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({job.delay() for i in range(10)}) > 1 diff --git a/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py b/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py new file mode 100644 index 0000000000..a826aed155 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py @@ -0,0 +1,365 @@ +# 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. + +import json +from unittest import mock + +import pytest + +from opentelemetry._opamp import messages +from opentelemetry._opamp.client import _HANDLED_CAPABILITIES, OpAMPClient +from opentelemetry._opamp.exceptions import ( + OpAMPRemoteConfigDecodeException, + OpAMPRemoteConfigParseException, +) +from opentelemetry._opamp.proto import opamp_pb2 +from opentelemetry._opamp.proto.anyvalue_pb2 import AnyValue as PB2AnyValue +from opentelemetry._opamp.proto.anyvalue_pb2 import KeyValue as PB2KeyValue +from opentelemetry._opamp.version import __version__ + + +@pytest.fixture(name="client") +def client_fixture(): + 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() + assert message.capabilities == _HANDLED_CAPABILITIES + + +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 + assert message.capabilities == _HANDLED_CAPABILITIES + + +def test_update_remote_config_status_without_previous_config(client): + remote_config_status = client._update_remote_config_status( + remote_config_hash=b"12345678", + status=opamp_pb2.RemoteConfigStatuses_APPLIED, + ) + + assert remote_config_status is not None + assert remote_config_status.last_remote_config_hash == b"12345678" + assert ( + remote_config_status.status == opamp_pb2.RemoteConfigStatuses_APPLIED + ) + assert remote_config_status.error_message == "" + + +def test_update_remote_config_status_with_same_config(client): + remote_config_status = client._update_remote_config_status( + remote_config_hash=b"12345678", + status=opamp_pb2.RemoteConfigStatuses_APPLIED, + ) + + assert remote_config_status is not None + + remote_config_status = client._update_remote_config_status( + remote_config_hash=b"12345678", + status=opamp_pb2.RemoteConfigStatuses_APPLIED, + ) + + assert remote_config_status is None + + +def test_update_remote_config_status_with_diffent_config(client): + remote_config_status = client._update_remote_config_status( + remote_config_hash=b"12345678", + status=opamp_pb2.RemoteConfigStatuses_APPLIED, + ) + + assert remote_config_status is not None + + # different status + remote_config_status = client._update_remote_config_status( + remote_config_hash=b"12345678", + status=opamp_pb2.RemoteConfigStatuses_FAILED, + ) + + assert remote_config_status is not None + + # different error message + remote_config_status = client._update_remote_config_status( + remote_config_hash=b"12345678", + status=opamp_pb2.RemoteConfigStatuses_FAILED, + error_message="different error message", + ) + + assert remote_config_status is not None + + # different hash + remote_config_status = client._update_remote_config_status( + remote_config_hash=b"1234", + status=opamp_pb2.RemoteConfigStatuses_FAILED, + error_message="different error message", + ) + + assert remote_config_status is not None + + +def test_build_remote_config_status_response_message_no_error_message(client): + remote_config_status = messages._build_remote_config_status_message( + last_remote_config_hash=b"12345678", + status=opamp_pb2.RemoteConfigStatuses_APPLIED, + ) + data = client._build_remote_config_status_response_message( + remote_config_status + ) + + message = opamp_pb2.AgentToServer() + message.ParseFromString(data) + + assert message + assert message.instance_uid == client._instance_uid + assert message.sequence_num == 0 + assert message.capabilities == _HANDLED_CAPABILITIES + assert message.remote_config_status + assert message.remote_config_status.last_remote_config_hash == b"12345678" + assert ( + message.remote_config_status.status + == opamp_pb2.RemoteConfigStatuses_APPLIED + ) + assert not message.remote_config_status.error_message + + +def test_build_remote_config_status_response_message_with_error_message( + client, +): + remote_config_status = messages._build_remote_config_status_message( + last_remote_config_hash=b"12345678", + status=opamp_pb2.RemoteConfigStatuses_FAILED, + error_message="an error message", + ) + data = client._build_remote_config_status_response_message( + remote_config_status + ) + + message = opamp_pb2.AgentToServer() + message.ParseFromString(data) + + assert message + assert message.instance_uid == client._instance_uid + assert message.sequence_num == 0 + assert message.capabilities == _HANDLED_CAPABILITIES + assert message.remote_config_status + assert message.remote_config_status.last_remote_config_hash == b"12345678" + assert ( + message.remote_config_status.status + == opamp_pb2.RemoteConfigStatuses_FAILED + ) + assert message.remote_config_status.error_message == "an error message" + + +def test_message_sequence_num_increases_in_send(client): + client._transport = mock.Mock() + for index in range(2): + data = client._build_heartbeat_message() + client._send(data) + + message = opamp_pb2.AgentToServer() + message.ParseFromString(data) + + assert message + assert message.sequence_num == index + + +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 = list(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/opamp/opentelemetry-opamp-client/tests/opamp/test_e2e.py b/opamp/opentelemetry-opamp-client/tests/opamp/test_e2e.py new file mode 100644 index 0000000000..3884e69dd7 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/tests/opamp/test_e2e.py @@ -0,0 +1,115 @@ +# 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. + +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 +from opentelemetry._opamp.proto import opamp_pb2 + + +@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_remote_config_status_heartbeat_disconnection(caplog): + caplog.set_level(logging.DEBUG, logger="opentelemetry._opamp.agent") + + def opamp_handler(agent, client, message): + logger = logging.getLogger("opentelemetry._opamp.agent.opamp_handler") + + logger.debug("In opamp_handler") + + # we need to update the config only if we have a config + if not message.remote_config.config_hash: + return + + updated_remote_config = client._update_remote_config_status( + remote_config_hash=message.remote_config.config_hash, + status=opamp_pb2.RemoteConfigStatuses_APPLIED, + error_message="", + ) + if updated_remote_config is not None: + logger.debug("Updated Remote Config") + message = client._build_remote_config_status_response_message( + updated_remote_config + ) + agent.send(payload=message) + + 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() + + handler_records = [ + record[2] + for record in caplog.record_tuples + if record[0] == "opentelemetry._opamp.agent.opamp_handler" + ] + # one call is for connection, one is remote config status, one is heartbeat + assert handler_records == [ + "In opamp_handler", + "Updated Remote Config", + "In opamp_handler", + "In opamp_handler", + ] + + +@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/opamp/opentelemetry-opamp-client/tests/opamp/transport/test_requests.py b/opamp/opentelemetry-opamp-client/tests/opamp/transport/test_requests.py new file mode 100644 index 0000000000..75a8632b94 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/tests/opamp/transport/test_requests.py @@ -0,0 +1,79 @@ +# 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. + +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, + ) diff --git a/scripts/opamp_proto_codegen.sh b/scripts/opamp_proto_codegen.sh new file mode 100755 index 0000000000..fe6dabcc4c --- /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/opamp/opentelemetry-opamp-client/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/tox.ini b/tox.ini index 4004b554d5..cda6a8c0bc 100644 --- a/tox.ini +++ b/tox.ini @@ -427,6 +427,12 @@ envlist = ; requires snappy headers to be available on the system lint-processor-baggage + ; opentelemetry-opamp-client + py3{9,10,11,12,13}-test-opamp-client + ; https://github.com/kevin1024/vcrpy/pull/775#issuecomment-1847849962 + ; pypy3-test-opamp-client + lint-opamp-client + spellcheck docker-tests docs @@ -720,6 +726,11 @@ deps = processor-baggage: {[testenv]test_deps} processor-baggage: -r {toxinidir}/processor/opentelemetry-processor-baggage/test-requirements.txt + opamp-client-oldest: -r {toxinidir}/opamp/opentelemetry-opamp-client/test-requirements.oldest.txt + opamp-client-latest: {[testenv]test_deps} + opamp-client-latest: -r {toxinidir}/opamp/opentelemetry-opamp-client/test-requirements.latest.txt + lint-opamp-client: -r {toxinidir}/opamp/opentelemetry-opamp-client/test-requirements.oldest.txt + util-http: {[testenv]test_deps} util-http: -r {toxinidir}/util/opentelemetry-util-http/test-requirements.txt util-http: {toxinidir}/util/opentelemetry-util-http @@ -958,6 +969,9 @@ commands = test-exporter-prometheus-remote-write: pytest {toxinidir}/exporter/opentelemetry-exporter-prometheus-remote-write/tests {posargs} lint-exporter-prometheus-remote-write: sh -c "cd exporter && pylint --rcfile ../.pylintrc opentelemetry-exporter-prometheus-remote-write" + test-opamp-client: pytest {toxinidir}/opamp/opentelemetry-opamp-client/tests {posargs} + lint-opamp-clent: sh -c "cd opamp && pylint --rcfile ../.pylintrc opentelemetry-opamp-client" + coverage: {toxinidir}/scripts/coverage.sh [testenv:docs] @@ -1078,6 +1092,7 @@ deps = {toxinidir}/instrumentation/opentelemetry-instrumentation-aiokafka[instruments] {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncclick[instruments] {toxinidir}/exporter/opentelemetry-exporter-credential-provider-gcp + {toxinidir}/opamp/opentelemetry-opamp-client commands = pyright From acd8b64d9f75cbfae46708911afe58a09fbabf44 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 30 Jul 2025 18:41:49 +0200 Subject: [PATCH 02/39] Add some docs and hook it into the system Still not building content --- docs/conf.py | 24 +++++-- docs/index.rst | 8 +++ docs/opamp/client.rst | 7 ++ opamp/opentelemetry-opamp-client/MANIFEST.rst | 9 +++ opamp/opentelemetry-opamp-client/README.rst | 2 + .../src/opentelemetry/_opamp/agent.py | 72 +++++++++++++++++++ .../src/opentelemetry/_opamp/client.py | 4 ++ 7 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 docs/opamp/client.rst create mode 100644 opamp/opentelemetry-opamp-client/MANIFEST.rst diff --git a/docs/conf.py b/docs/conf.py index 0f45647dc4..da0496a81c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,21 +28,21 @@ exp = "../exporter" exp_dirs = [ - os.path.abspath("/".join(["../exporter", f, "src"])) + os.path.abspath("/".join([exp, f, "src"])) for f in listdir(exp) if isdir(join(exp, f)) ] instr = "../instrumentation" instr_dirs = [ - os.path.abspath("/".join(["../instrumentation", f, "src"])) + os.path.abspath("/".join([instr, f, "src"])) for f in listdir(instr) - if isdir(join(instr, f)) + if isdir(join(instr, f)) and f != "opentelemetry-instrumentation-boto" ] instr_genai = "../instrumentation-genai" instr_genai_dirs = [ - os.path.abspath("/".join(["../instrumentation-genai", f, "src"])) + os.path.abspath("/".join([instr_genai, f, "src"])) for f in listdir(instr_genai) if isdir(join(instr_genai, f)) ] @@ -56,23 +56,32 @@ sdk_ext = "../sdk-extension" sdk_ext_dirs = [ - os.path.abspath("/".join(["../sdk-extension", f, "src"])) + os.path.abspath("/".join([sdk_ext, f, "src"])) for f in listdir(sdk_ext) if isdir(join(sdk_ext, f)) ] resource = "../resource" resource_dirs = [ - os.path.abspath("/".join(["../resource", f, "src"])) + os.path.abspath("/".join([resource, f, "src"])) for f in listdir(resource) if isdir(join(resource, f)) ] + util = "../util" util_dirs = [ os.path.abspath("/".join([util, f, "src"])) for f in listdir(util) if isdir(join(util, f)) ] + +opamp = "../opamp" +opamp_dirs = [ + os.path.abspath("/".join([opamp, f, "src"])) + for f in listdir(opamp) + if isdir(join(opamp, f)) +] + sys.path[:0] = ( exp_dirs + instr_dirs @@ -81,6 +90,7 @@ + prop_dirs + resource_dirs + util_dirs + + opamp_dirs ) # -- Project information ----------------------------------------------------- @@ -122,7 +132,7 @@ "https://opentracing-python.readthedocs.io/en/latest/", None, ), - "aiohttp": ("https://aiohttp.readthedocs.io/en/stable/", None), + "aiohttp": ("https://docs.aiohttp.org/en/stable/", None), "wrapt": ("https://wrapt.readthedocs.io/en/latest/", None), "pymongo": ("https://pymongo.readthedocs.io/en/stable/", None), "opentelemetry": ( diff --git a/docs/index.rst b/docs/index.rst index 11c611685b..1ac7f8179c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -103,6 +103,14 @@ install resource/** +.. toctree:: + :maxdepth: 2 + :caption: OpAMP + :name: OpAMP + :glob: + + opamp/** + Indices and tables ------------------ diff --git a/docs/opamp/client.rst b/docs/opamp/client.rst new file mode 100644 index 0000000000..b363f32867 --- /dev/null +++ b/docs/opamp/client.rst @@ -0,0 +1,7 @@ +OpenTelemetry Python - OpAMP Client +=================================== + +.. automodule:: opentelemetry._opamp + :members: agent, client + :undoc-members: + :show-inheritance: diff --git a/opamp/opentelemetry-opamp-client/MANIFEST.rst b/opamp/opentelemetry-opamp-client/MANIFEST.rst new file mode 100644 index 0000000000..2906eeef0f --- /dev/null +++ b/opamp/opentelemetry-opamp-client/MANIFEST.rst @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE \ No newline at end of file diff --git a/opamp/opentelemetry-opamp-client/README.rst b/opamp/opentelemetry-opamp-client/README.rst index 9984fc90d0..32aef2c5e3 100644 --- a/opamp/opentelemetry-opamp-client/README.rst +++ b/opamp/opentelemetry-opamp-client/README.rst @@ -14,6 +14,8 @@ Installation pip install opentelemetry-opamp-client +This package provides an HTTP OpAMP client than can be used by OpenTelemetry distributions to provide remote config. + References ---------- * `OpenTelemetry OpAMP Client `_ diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py index a81f94a870..ee84e6dbce 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py @@ -27,6 +27,78 @@ logger = logging.getLogger(__name__) +""" +OpenTelemetry Python - OpAMP client +----------------------------------- + +This package provides a bunch of classes that can be used by OpenTelemetry distributions implementors +to implement remote config support via the OpAMP protocol. + +The client implements the following capabilities: +- ReportsStatus +- ReportsHeartbeat +- AcceptsRemoteConfig +- ReportsRemoteConfig + +These capabilities are enough to get a remote config from an opamp server, parse it, apply it and ack it. + +Since OpAMP APIs, config options or environment variables are not standardizes the distros are required +to provide code doing so. +OTel Python distros would need to provide their own message handler callback that implements the actual +change of whatever configuration their backends sends. + +Please note that the API is not finalized yet and so the name is called ``_opamp`` with the underscore. + +Usage +----- + +.. code-block:: python + + import os + + from opentelemetry._opamp import messages + from opentelemetry._opamp.agent import OpAMPAgent + from opentelemetry._opamp.client import OpAMPClient + from opentelemetry._opamp.proto import opamp_pb2 as opamp_pb2 + from opentelemetry.sdk._configuration import _OTelSDKConfigurator + from opentelemetry.sdk.resources import OTELResourceDetector + + + def opamp_handler(agent: OpAMPAgent, client: OpAMPClient, message: opamp_pb2.ServerToAgent): + for config_filename, config in messages._decode_remote_config(message.remote_config): + print("do something") + + + class MyOpenTelemetryConfigurator(_OTelSDKConfigurator): + def _configure(self, **kwargs): + super()._configure(**kwargs) + + enable_opamp = False + endpoint = os.environ.get("OTEL_PYTHON_OPAMP_ENDPOINT") + if endpoint: + # this is not great but we don't have the calculated resource attributes around + # see https://github.com/open-telemetry/opentelemetry-python/pull/4646 for creating + # an entry point distros can implement + resource = OTELResourceDetector().detect() + agent_identifying_attributes = { + "service.name": resource.attributes.get("service.name"), + } + opamp_client = OpAMPClient( + endpoint=endpoint, + agent_identifying_attributes=agent_identifying_attributes, + ) + opamp_agent = OpAMPAgent( + interval=30, + message_handler=opamp_handler, + client=opamp_client, + ) + opamp_agent.start() + +API +--- +""" + + class _Job: """ Represents a single request job, with retry/backoff metadata. diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py index deaccead84..2438d5026f 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py @@ -43,6 +43,10 @@ class OpAMPClient: + """ + OpAMPClient implement the helpers for building and sending messages that the agent will use. + """ + def __init__( self, *, From efe881159e573af4bd94953dde86b585e6cf256c Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 30 Jul 2025 18:58:05 +0200 Subject: [PATCH 03/39] Add default value of 30 seconds to heartbeat message interval --- .../src/opentelemetry/_opamp/agent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py index ee84e6dbce..c867bf91df 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py @@ -144,7 +144,7 @@ class OpAMPAgent: def __init__( self, *, - interval: float, + interval: float = 30, message_handler: Callable[ ["OpAMPAgent", OpAMPClient, opamp_pb2.ServerToAgent], None ], @@ -154,7 +154,7 @@ def __init__( client: OpAMPClient, ): """ - :param interval: seconds between automatic calls + :param interval: seconds between heartbeat 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 From 5db6d7a916c3ff31879e4541ccda441a406175c8 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 30 Jul 2025 20:59:22 +0200 Subject: [PATCH 04/39] Fix docs build --- docs-requirements.txt | 5 ++ docs/opamp/client.rst | 2 +- .../src/opentelemetry/_opamp/__init__.py | 90 +++++++++++++++++++ .../src/opentelemetry/_opamp/agent.py | 72 --------------- 4 files changed, 96 insertions(+), 73 deletions(-) diff --git a/docs-requirements.txt b/docs-requirements.txt index afd03672d0..9d142f01e7 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -47,6 +47,11 @@ sqlalchemy>=1.0 tornado>=5.1.1 tortoise-orm>=0.17.0 +# required by opamp +uuid_utils +protobuf>=5.0,< 7.0 + # indirect dependency pins markupsafe==2.0.1 itsdangerous==2.0.1 + diff --git a/docs/opamp/client.rst b/docs/opamp/client.rst index b363f32867..74ababd2db 100644 --- a/docs/opamp/client.rst +++ b/docs/opamp/client.rst @@ -2,6 +2,6 @@ OpenTelemetry Python - OpAMP Client =================================== .. automodule:: opentelemetry._opamp - :members: agent, client + :members: :undoc-members: :show-inheritance: diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py index e69de29bb2..4666e5636e 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py @@ -0,0 +1,90 @@ +# 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. + +""" +OpenTelemetry Python - OpAMP client +----------------------------------- + +This package provides a bunch of classes that can be used by OpenTelemetry distributions implementors +to implement remote config support via the OpAMP protocol. + +The client implements the following capabilities: + +* ReportsStatus +* ReportsHeartbeat +* AcceptsRemoteConfig +* ReportsRemoteConfig + +These capabilities are enough to get a remote config from an opamp server, parse it, apply it and ack it. + +Since OpAMP APIs, config options or environment variables are not standardizes the distros are required +to provide code doing so. +OTel Python distros would need to provide their own message handler callback that implements the actual +change of whatever configuration their backends sends. + +Please note that the API is not finalized yet and so the name is called ``_opamp`` with the underscore. + +Usage +----- + +.. code-block:: python + + import os + + from opentelemetry._opamp import messages + from opentelemetry._opamp.agent import OpAMPAgent + from opentelemetry._opamp.client import OpAMPClient + from opentelemetry._opamp.proto import opamp_pb2 as opamp_pb2 + from opentelemetry.sdk._configuration import _OTelSDKConfigurator + from opentelemetry.sdk.resources import OTELResourceDetector + + + def opamp_handler(agent: OpAMPAgent, client: OpAMPClient, message: opamp_pb2.ServerToAgent): + for config_filename, config in messages._decode_remote_config(message.remote_config): + print("do something") + + + class MyOpenTelemetryConfigurator(_OTelSDKConfigurator): + def _configure(self, **kwargs): + super()._configure(**kwargs) + + enable_opamp = False + endpoint = os.environ.get("OTEL_PYTHON_OPAMP_ENDPOINT") + if endpoint: + # this is not great but we don't have the calculated resource attributes around + # see https://github.com/open-telemetry/opentelemetry-python/pull/4646 for creating + # an entry point distros can implement + resource = OTELResourceDetector().detect() + agent_identifying_attributes = { + "service.name": resource.attributes.get("service.name"), + } + opamp_client = OpAMPClient( + endpoint=endpoint, + agent_identifying_attributes=agent_identifying_attributes, + ) + opamp_agent = OpAMPAgent( + interval=30, + message_handler=opamp_handler, + client=opamp_client, + ) + opamp_agent.start() + +API +--- +""" + +from opentelemetry._opamp.agent import OpAMPAgent +from opentelemetry._opamp.client import OpAMPClient + +__all__ = ["OpAMPAgent", "OpAMPClient"] diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py index c867bf91df..87600d2cb6 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py @@ -27,78 +27,6 @@ logger = logging.getLogger(__name__) -""" -OpenTelemetry Python - OpAMP client ------------------------------------ - -This package provides a bunch of classes that can be used by OpenTelemetry distributions implementors -to implement remote config support via the OpAMP protocol. - -The client implements the following capabilities: -- ReportsStatus -- ReportsHeartbeat -- AcceptsRemoteConfig -- ReportsRemoteConfig - -These capabilities are enough to get a remote config from an opamp server, parse it, apply it and ack it. - -Since OpAMP APIs, config options or environment variables are not standardizes the distros are required -to provide code doing so. -OTel Python distros would need to provide their own message handler callback that implements the actual -change of whatever configuration their backends sends. - -Please note that the API is not finalized yet and so the name is called ``_opamp`` with the underscore. - -Usage ------ - -.. code-block:: python - - import os - - from opentelemetry._opamp import messages - from opentelemetry._opamp.agent import OpAMPAgent - from opentelemetry._opamp.client import OpAMPClient - from opentelemetry._opamp.proto import opamp_pb2 as opamp_pb2 - from opentelemetry.sdk._configuration import _OTelSDKConfigurator - from opentelemetry.sdk.resources import OTELResourceDetector - - - def opamp_handler(agent: OpAMPAgent, client: OpAMPClient, message: opamp_pb2.ServerToAgent): - for config_filename, config in messages._decode_remote_config(message.remote_config): - print("do something") - - - class MyOpenTelemetryConfigurator(_OTelSDKConfigurator): - def _configure(self, **kwargs): - super()._configure(**kwargs) - - enable_opamp = False - endpoint = os.environ.get("OTEL_PYTHON_OPAMP_ENDPOINT") - if endpoint: - # this is not great but we don't have the calculated resource attributes around - # see https://github.com/open-telemetry/opentelemetry-python/pull/4646 for creating - # an entry point distros can implement - resource = OTELResourceDetector().detect() - agent_identifying_attributes = { - "service.name": resource.attributes.get("service.name"), - } - opamp_client = OpAMPClient( - endpoint=endpoint, - agent_identifying_attributes=agent_identifying_attributes, - ) - opamp_agent = OpAMPAgent( - interval=30, - message_handler=opamp_handler, - client=opamp_client, - ) - opamp_agent.start() - -API ---- -""" - - class _Job: """ Represents a single request job, with retry/backoff metadata. From 1e3b43f6af3a40cf442d6fc2e7c058f0fa8c7f46 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 30 Jul 2025 21:13:21 +0200 Subject: [PATCH 05/39] More docs improvements --- .../src/opentelemetry/_opamp/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py index 4666e5636e..8051fae53e 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py @@ -17,7 +17,7 @@ ----------------------------------- This package provides a bunch of classes that can be used by OpenTelemetry distributions implementors -to implement remote config support via the OpAMP protocol. +to implement remote config support via the `OpAMP protocol`_. The client implements the following capabilities: @@ -28,6 +28,10 @@ These capabilities are enough to get a remote config from an opamp server, parse it, apply it and ack it. +While the client supports pluggable transports, only an HTTP backends using the ``requests`` library is +implemented. Adding WebSocket support shouldn't be hard but it will require some rework in the OpAMPAgent +class. + Since OpAMP APIs, config options or environment variables are not standardizes the distros are required to provide code doing so. OTel Python distros would need to provide their own message handler callback that implements the actual @@ -82,6 +86,7 @@ def _configure(self, **kwargs): API --- +.. _OpAMP protocol: https://opentelemetry.io/docs/specs/opamp/ """ from opentelemetry._opamp.agent import OpAMPAgent From c5d11c0e37d9c8b9b0e2b285193659dc06232e14 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 30 Jul 2025 21:16:33 +0200 Subject: [PATCH 06/39] Fix spellcheck --- .../src/opentelemetry/_opamp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py index 8051fae53e..a63e6a1804 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py @@ -16,7 +16,7 @@ OpenTelemetry Python - OpAMP client ----------------------------------- -This package provides a bunch of classes that can be used by OpenTelemetry distributions implementors +This package provides a bunch of classes that can be used by OpenTelemetry distributions implementers to implement remote config support via the `OpAMP protocol`_. The client implements the following capabilities: From 52ec0dcd5c182f7003d0abb1e58e9325a467e899 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 30 Jul 2025 21:16:38 +0200 Subject: [PATCH 07/39] Remove local workaround --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index da0496a81c..cf2e9c108f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -37,7 +37,7 @@ instr_dirs = [ os.path.abspath("/".join([instr, f, "src"])) for f in listdir(instr) - if isdir(join(instr, f)) and f != "opentelemetry-instrumentation-boto" + if isdir(join(instr, f)) ] instr_genai = "../instrumentation-genai" From 9a1ac49fa33906d3dbbe2488b310c5281aea9596 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 30 Jul 2025 21:17:56 +0200 Subject: [PATCH 08/39] Generate workflows and add to release script --- .github/workflows/core_contrib_test_0.yml | 30 +++++++ .github/workflows/lint_0.yml | 19 +++++ .github/workflows/test_2.yml | 95 +++++++++++++++++++++++ scripts/build.sh | 2 +- 4 files changed, 145 insertions(+), 1 deletion(-) diff --git a/.github/workflows/core_contrib_test_0.yml b/.github/workflows/core_contrib_test_0.yml index 40fc729620..aa478b69b4 100644 --- a/.github/workflows/core_contrib_test_0.yml +++ b/.github/workflows/core_contrib_test_0.yml @@ -2992,3 +2992,33 @@ jobs: - name: Run tests run: tox -e py39-test-processor-baggage -- -ra + + py39-test-opamp-client: + name: opamp-client + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} + uses: actions/checkout@v4 + with: + repository: open-telemetry/opentelemetry-python-contrib + ref: ${{ env.CONTRIB_REPO_SHA }} + + - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} + uses: actions/checkout@v4 + with: + repository: open-telemetry/opentelemetry-python + ref: ${{ env.CORE_REPO_SHA }} + path: opentelemetry-python + + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: "3.9" + architecture: "x64" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py39-test-opamp-client -- -ra diff --git a/.github/workflows/lint_0.yml b/.github/workflows/lint_0.yml index 309705ae5a..8c56e0f83f 100644 --- a/.github/workflows/lint_0.yml +++ b/.github/workflows/lint_0.yml @@ -1304,3 +1304,22 @@ jobs: - name: Run tests run: tox -e lint-processor-baggage + + lint-opamp-client: + name: opamp-client + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e lint-opamp-client diff --git a/.github/workflows/test_2.yml b/.github/workflows/test_2.yml index 8a385e54f4..5d273ef62c 100644 --- a/.github/workflows/test_2.yml +++ b/.github/workflows/test_2.yml @@ -1171,3 +1171,98 @@ jobs: - name: Run tests run: tox -e pypy3-test-processor-baggage -- -ra + + py39-test-opamp-client_ubuntu-latest: + name: opamp-client 3.9 Ubuntu + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: "3.9" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py39-test-opamp-client -- -ra + + py310-test-opamp-client_ubuntu-latest: + name: opamp-client 3.10 Ubuntu + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py310-test-opamp-client -- -ra + + py311-test-opamp-client_ubuntu-latest: + name: opamp-client 3.11 Ubuntu + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py311-test-opamp-client -- -ra + + py312-test-opamp-client_ubuntu-latest: + name: opamp-client 3.12 Ubuntu + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py312-test-opamp-client -- -ra + + py313-test-opamp-client_ubuntu-latest: + name: opamp-client 3.13 Ubuntu + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py313-test-opamp-client -- -ra diff --git a/scripts/build.sh b/scripts/build.sh index c652e399b6..fd5a207cae 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -16,7 +16,7 @@ DISTDIR=dist mkdir -p $DISTDIR rm -rf ${DISTDIR:?}/* - for d in exporter/*/ opentelemetry-instrumentation/ opentelemetry-contrib-instrumentations/ opentelemetry-distro/ instrumentation/*/ processor/*/ propagator/*/ resource/*/ sdk-extension/*/ util/*/ ; do + for d in exporter/*/ opentelemetry-instrumentation/ opentelemetry-contrib-instrumentations/ opentelemetry-distro/ instrumentation/*/ processor/*/ propagator/*/ resource/*/ sdk-extension/*/ util/*/ opamp/*/ ; do ( echo "building $d" cd "$d" From 72f4bad2d487d50164786b902a842c4834927445 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 1 Aug 2025 16:46:48 +0200 Subject: [PATCH 09/39] Fix typos in opamp lint commands --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index cda6a8c0bc..313b21d03a 100644 --- a/tox.ini +++ b/tox.ini @@ -726,10 +726,10 @@ deps = processor-baggage: {[testenv]test_deps} processor-baggage: -r {toxinidir}/processor/opentelemetry-processor-baggage/test-requirements.txt - opamp-client-oldest: -r {toxinidir}/opamp/opentelemetry-opamp-client/test-requirements.oldest.txt + opamp-client-lowest: -r {toxinidir}/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt opamp-client-latest: {[testenv]test_deps} opamp-client-latest: -r {toxinidir}/opamp/opentelemetry-opamp-client/test-requirements.latest.txt - lint-opamp-client: -r {toxinidir}/opamp/opentelemetry-opamp-client/test-requirements.oldest.txt + lint-opamp-client: -r {toxinidir}/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt util-http: {[testenv]test_deps} util-http: -r {toxinidir}/util/opentelemetry-util-http/test-requirements.txt @@ -970,7 +970,7 @@ commands = lint-exporter-prometheus-remote-write: sh -c "cd exporter && pylint --rcfile ../.pylintrc opentelemetry-exporter-prometheus-remote-write" test-opamp-client: pytest {toxinidir}/opamp/opentelemetry-opamp-client/tests {posargs} - lint-opamp-clent: sh -c "cd opamp && pylint --rcfile ../.pylintrc opentelemetry-opamp-client" + lint-opamp-client: sh -c "cd opamp && pylint --rcfile ../.pylintrc opentelemetry-opamp-client" coverage: {toxinidir}/scripts/coverage.sh From 454da0b1621d92f395c21d95899466e972e8a04b Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 1 Aug 2025 16:51:40 +0200 Subject: [PATCH 10/39] Fix requirements for pylint --- opamp/opentelemetry-opamp-client/test-requirements.in | 3 +++ .../opentelemetry-opamp-client/test-requirements.lowest.txt | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/opamp/opentelemetry-opamp-client/test-requirements.in b/opamp/opentelemetry-opamp-client/test-requirements.in index 12929cc350..21fa071e6c 100644 --- a/opamp/opentelemetry-opamp-client/test-requirements.in +++ b/opamp/opentelemetry-opamp-client/test-requirements.in @@ -13,3 +13,6 @@ urllib3>=2.5.0 uuid-utils>=0.11.0 wrapt>=1.16.0 -e opamp/opentelemetry-opamp-client + +# for pylint +tomli>=1.1.0 diff --git a/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt b/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt index 5669ed119d..0a9c369f3d 100644 --- a/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt +++ b/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt @@ -51,8 +51,10 @@ requests==2.32.3 # via # -c dev-requirements.txt # -r opamp/opentelemetry-opamp-client/test-requirements.in -tomli==1.0.0 ; python_full_version < '3.11' - # via pytest +tomli==1.1.0 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # pytest urllib3==2.5.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in From f089d414ca64f6d9d7c10327693af4f75f75e36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Em=C3=ADdio=20Neto?= <9735060+emdneto@users.noreply.github.com> Date: Fri, 1 Aug 2025 08:28:23 -0300 Subject: [PATCH 11/39] Update opamp/opentelemetry-opamp-client/pyproject.toml --- opamp/opentelemetry-opamp-client/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/opamp/opentelemetry-opamp-client/pyproject.toml b/opamp/opentelemetry-opamp-client/pyproject.toml index 67db850890..f6442c6e34 100644 --- a/opamp/opentelemetry-opamp-client/pyproject.toml +++ b/opamp/opentelemetry-opamp-client/pyproject.toml @@ -26,6 +26,7 @@ classifiers = [ ] dependencies = [ "protobuf>=5.0, < 7.0", + "uuid-utils>=0.11.0, <1" ] [project.urls] From 71896fb205badc0c024f2a4d29fe9402a0a94196 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 1 Aug 2025 16:54:51 +0200 Subject: [PATCH 12/39] Recreate requirements --- .../test-requirements.latest.txt | 10 +++++++--- .../test-requirements.lowest.txt | 4 +++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/opamp/opentelemetry-opamp-client/test-requirements.latest.txt b/opamp/opentelemetry-opamp-client/test-requirements.latest.txt index 8e730e144f..9705d8f743 100644 --- a/opamp/opentelemetry-opamp-client/test-requirements.latest.txt +++ b/opamp/opentelemetry-opamp-client/test-requirements.latest.txt @@ -51,8 +51,10 @@ requests==2.32.3 # via # -c dev-requirements.txt # -r opamp/opentelemetry-opamp-client/test-requirements.in -tomli==2.2.1 ; python_full_version < '3.11' - # via pytest +tomli==2.2.1 + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # pytest typing-extensions==4.14.0 ; python_full_version < '3.11' # via # exceptiongroup @@ -63,7 +65,9 @@ urllib3==2.5.0 # requests # vcrpy uuid-utils==0.11.0 - # via -r opamp/opentelemetry-opamp-client/test-requirements.in + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # opentelemetry-opamp-client vcrpy==7.0.0 ; python_full_version >= '3.10' and platform_python_implementation != 'PyPy' # via # -r opamp/opentelemetry-opamp-client/test-requirements.in diff --git a/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt b/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt index 0a9c369f3d..e5af6f0afd 100644 --- a/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt +++ b/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt @@ -61,7 +61,9 @@ urllib3==2.5.0 # requests # vcrpy uuid-utils==0.11.0 - # via -r opamp/opentelemetry-opamp-client/test-requirements.in + # via + # -r opamp/opentelemetry-opamp-client/test-requirements.in + # opentelemetry-opamp-client vcrpy==6.0.2 ; python_full_version >= '3.10' and platform_python_implementation != 'PyPy' # via # -r opamp/opentelemetry-opamp-client/test-requirements.in From 0ca2209915e0c47fcd2b5cfb6907771b31152ea3 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 1 Aug 2025 17:03:16 +0200 Subject: [PATCH 13/39] Add missing opentelemetry-api dependency --- opamp/opentelemetry-opamp-client/pyproject.toml | 1 + .../test-requirements.latest.txt | 9 ++++++++- .../test-requirements.lowest.txt | 7 +++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/opamp/opentelemetry-opamp-client/pyproject.toml b/opamp/opentelemetry-opamp-client/pyproject.toml index f6442c6e34..31f10501c5 100644 --- a/opamp/opentelemetry-opamp-client/pyproject.toml +++ b/opamp/opentelemetry-opamp-client/pyproject.toml @@ -25,6 +25,7 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] dependencies = [ + "opentelemetry-api ~= 1.12", "protobuf>=5.0, < 7.0", "uuid-utils>=0.11.0, <1" ] diff --git a/opamp/opentelemetry-opamp-client/test-requirements.latest.txt b/opamp/opentelemetry-opamp-client/test-requirements.latest.txt index 9705d8f743..b529c4e22a 100644 --- a/opamp/opentelemetry-opamp-client/test-requirements.latest.txt +++ b/opamp/opentelemetry-opamp-client/test-requirements.latest.txt @@ -16,12 +16,16 @@ idna==3.10 # via # requests # yarl +importlib-metadata==8.7.0 + # via opentelemetry-api iniconfig==2.1.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # pytest multidict==6.6.3 # via yarl +opentelemetry-api==1.36.0 + # via opentelemetry-opamp-client packaging==25.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in @@ -55,10 +59,11 @@ tomli==2.2.1 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # pytest -typing-extensions==4.14.0 ; python_full_version < '3.11' +typing-extensions==4.14.0 # via # exceptiongroup # multidict + # opentelemetry-api urllib3==2.5.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in @@ -80,3 +85,5 @@ yarl==1.20.1 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # vcrpy +zipp==3.23.0 + # via importlib-metadata diff --git a/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt b/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt index e5af6f0afd..c203823ddd 100644 --- a/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt +++ b/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt @@ -10,6 +10,8 @@ colorama==0.4.6 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # pytest +deprecated==1.2.6 + # via opentelemetry-api exceptiongroup==1.0.0 ; python_full_version < '3.11' # via pytest idna==2.5 @@ -22,6 +24,8 @@ iniconfig==2.0.0 # pytest multidict==4.0.0 # via yarl +opentelemetry-api==1.12.0 + # via opentelemetry-opamp-client packaging==24.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in @@ -51,6 +55,8 @@ requests==2.32.3 # via # -c dev-requirements.txt # -r opamp/opentelemetry-opamp-client/test-requirements.in +setuptools==16.0 + # via opentelemetry-api tomli==1.1.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in @@ -71,6 +77,7 @@ vcrpy==6.0.2 ; python_full_version >= '3.10' and platform_python_implementation wrapt==1.16.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in + # deprecated # vcrpy yarl==1.20.1 # via From 9fa39bb6ae8eaad84e799dc4ab7781db1e05185a Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 1 Aug 2025 17:08:21 +0200 Subject: [PATCH 14/39] Fix tox test commands Drop opentelemetry api fixed version from requirements --- opamp/opentelemetry-opamp-client/test-requirements.latest.txt | 2 -- opamp/opentelemetry-opamp-client/test-requirements.lowest.txt | 2 -- tox.ini | 2 +- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/opamp/opentelemetry-opamp-client/test-requirements.latest.txt b/opamp/opentelemetry-opamp-client/test-requirements.latest.txt index b529c4e22a..8c2e295128 100644 --- a/opamp/opentelemetry-opamp-client/test-requirements.latest.txt +++ b/opamp/opentelemetry-opamp-client/test-requirements.latest.txt @@ -24,8 +24,6 @@ iniconfig==2.1.0 # pytest multidict==6.6.3 # via yarl -opentelemetry-api==1.36.0 - # via opentelemetry-opamp-client packaging==25.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in diff --git a/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt b/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt index c203823ddd..51bd2b3e4d 100644 --- a/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt +++ b/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt @@ -24,8 +24,6 @@ iniconfig==2.0.0 # pytest multidict==4.0.0 # via yarl -opentelemetry-api==1.12.0 - # via opentelemetry-opamp-client packaging==24.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in diff --git a/tox.ini b/tox.ini index 313b21d03a..fd6015ae15 100644 --- a/tox.ini +++ b/tox.ini @@ -726,8 +726,8 @@ deps = processor-baggage: {[testenv]test_deps} processor-baggage: -r {toxinidir}/processor/opentelemetry-processor-baggage/test-requirements.txt + opamp-client: {[testenv]test_deps} opamp-client-lowest: -r {toxinidir}/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt - opamp-client-latest: {[testenv]test_deps} opamp-client-latest: -r {toxinidir}/opamp/opentelemetry-opamp-client/test-requirements.latest.txt lint-opamp-client: -r {toxinidir}/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt From 404931c1b0aa6d1c29de143e91d9ded5c8de5071 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 25 Aug 2025 11:26:09 +0200 Subject: [PATCH 15/39] Fix tox --- .github/workflows/core_contrib_test_0.yml | 36 ++++++- .github/workflows/test_2.yml | 125 +++++++++++++++++++--- tox.ini | 2 +- 3 files changed, 144 insertions(+), 19 deletions(-) diff --git a/.github/workflows/core_contrib_test_0.yml b/.github/workflows/core_contrib_test_0.yml index aa478b69b4..301c85029f 100644 --- a/.github/workflows/core_contrib_test_0.yml +++ b/.github/workflows/core_contrib_test_0.yml @@ -2993,8 +2993,8 @@ jobs: - name: Run tests run: tox -e py39-test-processor-baggage -- -ra - py39-test-opamp-client: - name: opamp-client + py39-test-opamp-client-latest: + name: opamp-client-latest runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -3021,4 +3021,34 @@ jobs: run: pip install tox-uv - name: Run tests - run: tox -e py39-test-opamp-client -- -ra + run: tox -e py39-test-opamp-client-latest -- -ra + + py39-test-opamp-client-lowest: + name: opamp-client-lowest + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }} + uses: actions/checkout@v4 + with: + repository: open-telemetry/opentelemetry-python-contrib + ref: ${{ env.CONTRIB_REPO_SHA }} + + - name: Checkout core repo @ SHA - ${{ env.CORE_REPO_SHA }} + uses: actions/checkout@v4 + with: + repository: open-telemetry/opentelemetry-python + ref: ${{ env.CORE_REPO_SHA }} + path: opentelemetry-python + + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: "3.9" + architecture: "x64" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py39-test-opamp-client-lowest -- -ra diff --git a/.github/workflows/test_2.yml b/.github/workflows/test_2.yml index 5d273ef62c..a7a0b4a2af 100644 --- a/.github/workflows/test_2.yml +++ b/.github/workflows/test_2.yml @@ -1172,8 +1172,8 @@ jobs: - name: Run tests run: tox -e pypy3-test-processor-baggage -- -ra - py39-test-opamp-client_ubuntu-latest: - name: opamp-client 3.9 Ubuntu + py39-test-opamp-client-latest_ubuntu-latest: + name: opamp-client-latest 3.9 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -1189,10 +1189,48 @@ jobs: run: pip install tox-uv - name: Run tests - run: tox -e py39-test-opamp-client -- -ra + run: tox -e py39-test-opamp-client-latest -- -ra - py310-test-opamp-client_ubuntu-latest: - name: opamp-client 3.10 Ubuntu + py39-test-opamp-client-lowest_ubuntu-latest: + name: opamp-client-lowest 3.9 Ubuntu + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.9 + uses: actions/setup-python@v5 + with: + python-version: "3.9" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py39-test-opamp-client-lowest -- -ra + + py310-test-opamp-client-latest_ubuntu-latest: + name: opamp-client-latest 3.10 Ubuntu + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.10 + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py310-test-opamp-client-latest -- -ra + + py310-test-opamp-client-lowest_ubuntu-latest: + name: opamp-client-lowest 3.10 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -1208,10 +1246,29 @@ jobs: run: pip install tox-uv - name: Run tests - run: tox -e py310-test-opamp-client -- -ra + run: tox -e py310-test-opamp-client-lowest -- -ra + + py311-test-opamp-client-latest_ubuntu-latest: + name: opamp-client-latest 3.11 Ubuntu + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py311-test-opamp-client-latest -- -ra - py311-test-opamp-client_ubuntu-latest: - name: opamp-client 3.11 Ubuntu + py311-test-opamp-client-lowest_ubuntu-latest: + name: opamp-client-lowest 3.11 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -1227,10 +1284,29 @@ jobs: run: pip install tox-uv - name: Run tests - run: tox -e py311-test-opamp-client -- -ra + run: tox -e py311-test-opamp-client-lowest -- -ra + + py312-test-opamp-client-latest_ubuntu-latest: + name: opamp-client-latest 3.12 Ubuntu + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py312-test-opamp-client-latest -- -ra - py312-test-opamp-client_ubuntu-latest: - name: opamp-client 3.12 Ubuntu + py312-test-opamp-client-lowest_ubuntu-latest: + name: opamp-client-lowest 3.12 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -1246,10 +1322,29 @@ jobs: run: pip install tox-uv - name: Run tests - run: tox -e py312-test-opamp-client -- -ra + run: tox -e py312-test-opamp-client-lowest -- -ra + + py313-test-opamp-client-latest_ubuntu-latest: + name: opamp-client-latest 3.13 Ubuntu + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install tox + run: pip install tox-uv + + - name: Run tests + run: tox -e py313-test-opamp-client-latest -- -ra - py313-test-opamp-client_ubuntu-latest: - name: opamp-client 3.13 Ubuntu + py313-test-opamp-client-lowest_ubuntu-latest: + name: opamp-client-lowest 3.13 Ubuntu runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -1265,4 +1360,4 @@ jobs: run: pip install tox-uv - name: Run tests - run: tox -e py313-test-opamp-client -- -ra + run: tox -e py313-test-opamp-client-lowest -- -ra diff --git a/tox.ini b/tox.ini index fd6015ae15..df588b5af0 100644 --- a/tox.ini +++ b/tox.ini @@ -428,7 +428,7 @@ envlist = lint-processor-baggage ; opentelemetry-opamp-client - py3{9,10,11,12,13}-test-opamp-client + py3{9,10,11,12,13}-test-opamp-client-{latest,lowest} ; https://github.com/kevin1024/vcrpy/pull/775#issuecomment-1847849962 ; pypy3-test-opamp-client lint-opamp-client From 40e671c985645e2a1054183161ac1814658fc8b0 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 25 Aug 2025 11:41:05 +0200 Subject: [PATCH 16/39] Add baseline of vcrpy 7.0.0 --- opamp/opentelemetry-opamp-client/test-requirements.in | 2 +- opamp/opentelemetry-opamp-client/test-requirements.lowest.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/opamp/opentelemetry-opamp-client/test-requirements.in b/opamp/opentelemetry-opamp-client/test-requirements.in index 21fa071e6c..67c1f299b8 100644 --- a/opamp/opentelemetry-opamp-client/test-requirements.in +++ b/opamp/opentelemetry-opamp-client/test-requirements.in @@ -6,7 +6,7 @@ protobuf>=5.29.5 pytest>=7.4.4 pytest-vcr>=1.0.2 ; python_version > '3.9' and platform_python_implementation !='PyPy' pyyaml>=6.0.2 -vcrpy>=6.0.2 ; python_version > '3.9' and platform_python_implementation !='PyPy' +vcrpy>=7.0.0 ; python_version > '3.9' and platform_python_implementation !='PyPy' yarl>=1.20.1 requests>=2.32.2 urllib3>=2.5.0 diff --git a/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt b/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt index 51bd2b3e4d..914aae8955 100644 --- a/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt +++ b/opamp/opentelemetry-opamp-client/test-requirements.lowest.txt @@ -68,7 +68,7 @@ uuid-utils==0.11.0 # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # opentelemetry-opamp-client -vcrpy==6.0.2 ; python_full_version >= '3.10' and platform_python_implementation != 'PyPy' +vcrpy==7.0.0 ; python_full_version >= '3.10' and platform_python_implementation != 'PyPy' # via # -r opamp/opentelemetry-opamp-client/test-requirements.in # pytest-vcr From acf5ad7b3fe9b9848cc28d7f7d5ef9ca136e8d17 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 25 Aug 2025 11:58:10 +0200 Subject: [PATCH 17/39] Ignore pb2 module in pylintrc --- .pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index 496e1d846b..33fcdc1983 100644 --- a/.pylintrc +++ b/.pylintrc @@ -179,7 +179,7 @@ contextmanager-decorators=contextlib.contextmanager # List of members which are set dynamically and missed by pylint inference # system, and so shouldn't trigger E1101 when accessed. Python regular # expressions are accepted. -generated-members=types_pb2.* +generated-members=types_pb2.*,anyvalue_pb2.*,opamp_pb2.* # Tells whether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). From 700088dc54a02394629649e7ae31e45b15b7cda3 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 25 Aug 2025 12:14:59 +0200 Subject: [PATCH 18/39] Bump pylint to match the version in core --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index b41f3d7b99..684da09b22 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,4 @@ -pylint==3.0.2 +pylint==3.3.4 httpretty==1.1.4 pyright==v1.1.404 sphinx==7.1.2 From 793c58afed82fa0023ba1f704781ee8d186498d7 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 25 Aug 2025 12:48:24 +0200 Subject: [PATCH 19/39] Silence pylint warnings --- .../src/opentelemetry/_opamp/messages.py | 8 ++++++-- .../opentelemetry-opamp-client/tests/opamp/test_client.py | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py index f9410d4a11..99d3ddbda3 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py @@ -22,8 +22,12 @@ OpAMPRemoteConfigParseException, ) from opentelemetry._opamp.proto import opamp_pb2 -from opentelemetry._opamp.proto.anyvalue_pb2 import AnyValue as PB2AnyValue -from opentelemetry._opamp.proto.anyvalue_pb2 import KeyValue as PB2KeyValue +from opentelemetry._opamp.proto.anyvalue_pb2 import ( + AnyValue as PB2AnyValue, # pylint: disable=no-name-in-module +) +from opentelemetry._opamp.proto.anyvalue_pb2 import ( + KeyValue as PB2KeyValue, # pylint: disable=no-name-in-module +) from opentelemetry.util.types import AnyValue diff --git a/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py b/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py index a826aed155..4971a0d318 100644 --- a/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py +++ b/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py @@ -24,8 +24,12 @@ OpAMPRemoteConfigParseException, ) from opentelemetry._opamp.proto import opamp_pb2 -from opentelemetry._opamp.proto.anyvalue_pb2 import AnyValue as PB2AnyValue -from opentelemetry._opamp.proto.anyvalue_pb2 import KeyValue as PB2KeyValue +from opentelemetry._opamp.proto.anyvalue_pb2 import ( + AnyValue as PB2AnyValue, # pylint: disable=no-name-in-module +) +from opentelemetry._opamp.proto.anyvalue_pb2 import ( + KeyValue as PB2KeyValue, # pylint: disable=no-name-in-module +) from opentelemetry._opamp.version import __version__ From 534e31713785b39f690dad5c87c92076e5207dfb Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 11 Sep 2025 11:26:55 +0200 Subject: [PATCH 20/39] Don't trace opamp client own http requests --- .../src/opentelemetry/_opamp/client.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py index 2438d5026f..d49ecd5237 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py @@ -23,6 +23,12 @@ from opentelemetry._opamp.proto import opamp_pb2 from opentelemetry._opamp.transport.requests import RequestsTransport from opentelemetry._opamp.version import __version__ +from opentelemetry.context import ( + _SUPPRESS_INSTRUMENTATION_KEY, + attach, + detach, + set_value, +) from opentelemetry.util.types import AnyValue _logger = getLogger(__name__) @@ -142,6 +148,7 @@ def _build_remote_config_status_response_message( return data def _send(self, data: bytes): + token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) try: response = self._transport.send( url=self._endpoint, @@ -152,6 +159,7 @@ def _send(self, data: bytes): return response finally: self._sequence_num += 1 + detach(token) @staticmethod def _decode_remote_config( From 4c08bc375416fc8ae870f38bab77b810591da13f Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 11 Sep 2025 11:42:19 +0200 Subject: [PATCH 21/39] Permit to pass a custom transport to client And a custom session to RequestsTransport --- .../src/opentelemetry/_opamp/client.py | 6 +++++- .../src/opentelemetry/_opamp/transport/requests.py | 6 ++++-- .../tests/opamp/test_client.py | 4 ++++ .../tests/opamp/transport/test_requests.py | 9 +++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py index d49ecd5237..63739fd10b 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py @@ -21,6 +21,7 @@ from opentelemetry._opamp import messages from opentelemetry._opamp.proto import opamp_pb2 +from opentelemetry._opamp.transport.base import HttpTransport from opentelemetry._opamp.transport.requests import RequestsTransport from opentelemetry._opamp.version import __version__ from opentelemetry.context import ( @@ -61,9 +62,12 @@ def __init__( timeout_millis: int = _DEFAULT_OPAMP_TIMEOUT_MS, agent_identifying_attributes: Mapping[str, AnyValue], agent_non_identifying_attributes: Mapping[str, AnyValue] | None = None, + transport: HttpTransport = None, ): self._timeout_millis = timeout_millis - self._transport = RequestsTransport() + self._transport = ( + RequestsTransport() if transport is None else transport + ) self._endpoint = endpoint headers = headers or {} diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/requests.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/requests.py index 6ce2c0c775..3af088a5de 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/requests.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/requests.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + from typing import Mapping import requests @@ -22,8 +24,8 @@ class RequestsTransport(HttpTransport): - def __init__(self): - self.session = requests.Session() + def __init__(self, session: requests.Session | None = None): + self.session = requests.Session() if session is None else session def send( self, diff --git a/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py b/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py index 4971a0d318..a53b42b370 100644 --- a/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py +++ b/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py @@ -30,6 +30,7 @@ from opentelemetry._opamp.proto.anyvalue_pb2 import ( KeyValue as PB2KeyValue, # pylint: disable=no-name-in-module ) +from opentelemetry._opamp.transport.requests import RequestsTransport from opentelemetry._opamp.version import __version__ @@ -61,12 +62,14 @@ def test_can_instantiate_opamp_client_with_defaults(): def test_can_instantiate_opamp_client_all_params(): + transport = RequestsTransport() client = OpAMPClient( endpoint="url", headers={"an": "header"}, timeout_millis=2_000, agent_identifying_attributes={"foo": "bar"}, agent_non_identifying_attributes={"bar": "baz"}, + transport=transport, ) assert client @@ -85,6 +88,7 @@ def test_can_instantiate_opamp_client_all_params(): assert client._agent_description.non_identifying_attributes == [ PB2KeyValue(key="bar", value=PB2AnyValue(string_value="baz")), ] + assert client._transport is transport def test_client_headers_override_defaults(): diff --git a/opamp/opentelemetry-opamp-client/tests/opamp/transport/test_requests.py b/opamp/opentelemetry-opamp-client/tests/opamp/transport/test_requests.py index 75a8632b94..a3dec9fe39 100644 --- a/opamp/opentelemetry-opamp-client/tests/opamp/transport/test_requests.py +++ b/opamp/opentelemetry-opamp-client/tests/opamp/transport/test_requests.py @@ -15,6 +15,7 @@ from unittest import mock import pytest +import requests from opentelemetry._opamp.proto import opamp_pb2 from opentelemetry._opamp.transport.base import base_headers @@ -28,6 +29,14 @@ def test_can_instantiate_requests_transport(): assert transport +def test_can_instantiate_requests_transport_with_own_session(): + session = requests.Session() + transport = RequestsTransport(session=session) + + assert transport + assert transport.session is session + + def test_can_send(): transport = RequestsTransport() serialized_message = opamp_pb2.ServerToAgent().SerializeToString() From 71a680162f6bf04cf1268defc0550f15c74f211b Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 11 Sep 2025 11:47:55 +0200 Subject: [PATCH 22/39] Don't bump pylint after all --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 684da09b22..b41f3d7b99 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,4 @@ -pylint==3.3.4 +pylint==3.0.2 httpretty==1.1.4 pyright==v1.1.404 sphinx==7.1.2 From 99f20ce4f2924c8c80d4b2dcb50d07017d078a1d Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 11 Sep 2025 12:02:23 +0200 Subject: [PATCH 23/39] Fix pylint --- .../src/opentelemetry/_opamp/messages.py | 6 ++++-- opamp/opentelemetry-opamp-client/tests/opamp/test_client.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py index 99d3ddbda3..451974051a 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=no-name-in-module + from __future__ import annotations import json @@ -23,10 +25,10 @@ ) from opentelemetry._opamp.proto import opamp_pb2 from opentelemetry._opamp.proto.anyvalue_pb2 import ( - AnyValue as PB2AnyValue, # pylint: disable=no-name-in-module + AnyValue as PB2AnyValue, ) from opentelemetry._opamp.proto.anyvalue_pb2 import ( - KeyValue as PB2KeyValue, # pylint: disable=no-name-in-module + KeyValue as PB2KeyValue, ) from opentelemetry.util.types import AnyValue diff --git a/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py b/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py index a53b42b370..5b47a9183f 100644 --- a/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py +++ b/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=no-name-in-module + import json from unittest import mock @@ -25,10 +27,10 @@ ) from opentelemetry._opamp.proto import opamp_pb2 from opentelemetry._opamp.proto.anyvalue_pb2 import ( - AnyValue as PB2AnyValue, # pylint: disable=no-name-in-module + AnyValue as PB2AnyValue, ) from opentelemetry._opamp.proto.anyvalue_pb2 import ( - KeyValue as PB2KeyValue, # pylint: disable=no-name-in-module + KeyValue as PB2KeyValue, ) from opentelemetry._opamp.transport.requests import RequestsTransport from opentelemetry._opamp.version import __version__ From 945b7b93c0016f7e6ad28efbf9fc1ada3adfaa5c Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 12 Sep 2025 11:09:59 +0200 Subject: [PATCH 24/39] Try to typecheck opamp client --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 30ada5bb25..6fc1cdadb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -206,6 +206,7 @@ include = [ "instrumentation-genai/opentelemetry-instrumentation-weaviate", "util/opentelemetry-util-genai", "exporter/opentelemetry-exporter-credential-provider-gcp", + "opamp/opentelemetry-opamp-client", ] # We should also add type hints to the test suite - It helps on finding bugs. # We are excluding for now because it's easier, and more important to add to the instrumentation packages. @@ -220,4 +221,6 @@ exclude = [ "instrumentation-genai/opentelemetry-instrumentation-weaviate/tests/**/*.py", "instrumentation-genai/opentelemetry-instrumentation-weaviate/examples/**/*.py", "util/opentelemetry-util-genai/tests/**/*.py", + "opamp/opentelemetry-opamp-client/tests/**/*.py", + "opamp/opentelemetry-opamp-client/src/opentelemetry/*/proto/*.py", ] From b010f9c0257505b42b3a2081deeddfbcecaa2065 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 12 Sep 2025 11:10:22 +0200 Subject: [PATCH 25/39] Bump version after rebase --- .../src/opentelemetry/_opamp/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/version.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/version.py index d556297f28..46aee9202b 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/version.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "0.57b0.dev" +__version__ = "0.59b0.dev" From b91c5d0162f6f9a92f3807289cc6fc857e9c0d53 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 12 Sep 2025 11:11:23 +0200 Subject: [PATCH 26/39] Fix typecheck in client --- .../src/opentelemetry/_opamp/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py index 63739fd10b..ab1dc48b03 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py @@ -62,7 +62,7 @@ def __init__( timeout_millis: int = _DEFAULT_OPAMP_TIMEOUT_MS, agent_identifying_attributes: Mapping[str, AnyValue], agent_non_identifying_attributes: Mapping[str, AnyValue] | None = None, - transport: HttpTransport = None, + transport: HttpTransport | None = None, ): self._timeout_millis = timeout_millis self._transport = ( From c55531ce43ba6f6265ed185e7f83aef0f6f245f2 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 12 Sep 2025 11:50:36 +0200 Subject: [PATCH 27/39] Please pyright in strict mode --- .../src/opentelemetry/_opamp/messages.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py index 451974051a..c4649e4cd6 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py @@ -13,6 +13,8 @@ # limitations under the License. # pylint: disable=no-name-in-module +# FIXME: remove this after _opamp -> opamp, making this helpers public is not enough for pyright +# type: ignore[reportUnusedFunction] from __future__ import annotations From 683d0cfaa11ab73daeff75f99e5a5afb8c9e5bc6 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 12 Sep 2025 16:22:04 +0200 Subject: [PATCH 28/39] No need for functions and methods to be private since _opamp module is already private --- .../src/opentelemetry/_opamp/__init__.py | 3 +- .../src/opentelemetry/_opamp/agent.py | 10 ++--- .../src/opentelemetry/_opamp/client.py | 36 +++++++-------- .../src/opentelemetry/_opamp/messages.py | 20 ++++----- .../_opamp/transport/requests.py | 2 +- .../tests/opamp/test_agent.py | 12 ++--- .../tests/opamp/test_client.py | 44 +++++++++---------- .../tests/opamp/test_e2e.py | 4 +- 8 files changed, 64 insertions(+), 67 deletions(-) diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py index a63e6a1804..d2773fb66e 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py @@ -46,7 +46,6 @@ import os - from opentelemetry._opamp import messages from opentelemetry._opamp.agent import OpAMPAgent from opentelemetry._opamp.client import OpAMPClient from opentelemetry._opamp.proto import opamp_pb2 as opamp_pb2 @@ -55,7 +54,7 @@ def opamp_handler(agent: OpAMPAgent, client: OpAMPClient, message: opamp_pb2.ServerToAgent): - for config_filename, config in messages._decode_remote_config(message.remote_config): + for config_filename, config in client.decode_remote_config(message.remote_config): print("do something") diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py index 87600d2cb6..525388c5f4 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py @@ -124,7 +124,7 @@ def start(self) -> None: atexit.register(self.stop) # enqueue the connection message so we can then enable heartbeat - payload = self._client._build_connection_message() + payload = self._client.build_connection_message() self.send( payload, max_retries=self._max_retries, @@ -162,7 +162,7 @@ def _run_scheduler(self) -> None: """ while not self._stop.wait(self._interval): if self._schedule: - payload = self._client._build_heartbeat_message() + payload = self._client.build_heartbeat_message() job = _Job( payload=payload, max_retries=self._heartbeat_max_retries, @@ -185,7 +185,7 @@ def _run_worker(self) -> None: message = None while job.should_retry() and not self._stop.is_set(): try: - message = self._client._send(job.payload) + message = self._client.send(job.payload) logger.debug("Job succeeded: %r", job.payload) break except Exception as exc: @@ -241,9 +241,9 @@ def stop(self) -> None: # 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() + payload = self._client.build_agent_disconnect_message() try: - self._client._send(payload) + self._client.send(payload) except Exception: # pylint: disable=broad-exception-caught logger.debug( "Stopping OpAMPClient: failed to send AgentDisconnect message" diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py index ab1dc48b03..3b22932b12 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py @@ -73,7 +73,7 @@ def __init__( headers = headers or {} self._headers = {**_OTLP_HTTP_HEADERS, **headers} - self._agent_description = messages._build_agent_description( + self._agent_description = messages.build_agent_description( identifying_attributes=agent_identifying_attributes, non_identifying_attributes=agent_non_identifying_attributes, ) @@ -81,35 +81,35 @@ def __init__( self._instance_uid: bytes = uuid7().bytes self._remote_config_status: opamp_pb2.RemoteConfigStatus | None = None - def _build_connection_message(self) -> bytes: - message = messages._build_presentation_message( + 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) + data = messages.encode_message(message) return data - def _build_agent_disconnect_message(self) -> bytes: - message = messages._build_agent_disconnect_message( + 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) + data = messages.encode_message(message) return data - def _build_heartbeat_message(self) -> bytes: - message = messages._build_heartbeat_message( + 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) + data = messages.encode_message(message) return data - def _update_remote_config_status( + def update_remote_config_status( self, remote_config_hash: bytes, status: opamp_pb2.RemoteConfigStatuses.ValueType, @@ -129,7 +129,7 @@ def _update_remote_config_status( remote_config_hash, ) self._remote_config_status = ( - messages._build_remote_config_status_message( + messages.build_remote_config_status_message( last_remote_config_hash=remote_config_hash, status=status, error_message=error_message, @@ -139,19 +139,19 @@ def _update_remote_config_status( return None - def _build_remote_config_status_response_message( + def build_remote_config_status_response_message( self, remote_config_status: opamp_pb2.RemoteConfigStatus ) -> bytes: - message = messages._build_remote_config_status_response_message( + message = messages.build_remote_config_status_response_message( instance_uid=self._instance_uid, sequence_num=self._sequence_num, capabilities=_HANDLED_CAPABILITIES, remote_config_status=remote_config_status, ) - data = messages._encode_message(message) + data = messages.encode_message(message) return data - def _send(self, data: bytes): + def send(self, data: bytes): token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) try: response = self._transport.send( @@ -166,10 +166,10 @@ def _send(self, data: bytes): detach(token) @staticmethod - def _decode_remote_config( + def decode_remote_config( remote_config: opamp_pb2.AgentRemoteConfig, ) -> Generator[tuple[str, Mapping[str, AnyValue]]]: - for config_file, config in messages._decode_remote_config( + for config_file, config in messages.decode_remote_config( remote_config ): yield config_file, config diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py index c4649e4cd6..df7ee95cba 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py @@ -13,8 +13,6 @@ # limitations under the License. # pylint: disable=no-name-in-module -# FIXME: remove this after _opamp -> opamp, making this helpers public is not enough for pyright -# type: ignore[reportUnusedFunction] from __future__ import annotations @@ -35,7 +33,7 @@ from opentelemetry.util.types import AnyValue -def _decode_message(data: bytes) -> opamp_pb2.ServerToAgent: +def decode_message(data: bytes) -> opamp_pb2.ServerToAgent: message = opamp_pb2.ServerToAgent() message.ParseFromString(data) return message @@ -65,7 +63,7 @@ def _encode_attributes(attributes: Mapping[str, AnyValue]): ] -def _build_agent_description( +def build_agent_description( identifying_attributes: Mapping[str, AnyValue], non_identifying_attributes: Mapping[str, AnyValue] | None = None, ) -> opamp_pb2.AgentDescription: @@ -81,7 +79,7 @@ def _build_agent_description( ) -def _build_presentation_message( +def build_presentation_message( instance_uid: bytes, sequence_num: int, agent_description: opamp_pb2.AgentDescription, @@ -96,7 +94,7 @@ def _build_presentation_message( return command -def _build_heartbeat_message( +def build_heartbeat_message( instance_uid: bytes, sequence_num: int, capabilities: int ) -> opamp_pb2.AgentToServer: command = opamp_pb2.AgentToServer( @@ -107,7 +105,7 @@ def _build_heartbeat_message( return command -def _build_agent_disconnect_message( +def build_agent_disconnect_message( instance_uid: bytes, sequence_num: int, capabilities: int ) -> opamp_pb2.AgentToServer: command = opamp_pb2.AgentToServer( @@ -119,7 +117,7 @@ def _build_agent_disconnect_message( return command -def _build_remote_config_status_message( +def build_remote_config_status_message( last_remote_config_hash: bytes, status: opamp_pb2.RemoteConfigStatuses.ValueType, error_message: str = "", @@ -131,7 +129,7 @@ def _build_remote_config_status_message( ) -def _build_remote_config_status_response_message( +def build_remote_config_status_response_message( instance_uid: bytes, sequence_num: int, capabilities: int, @@ -146,11 +144,11 @@ def _build_remote_config_status_response_message( return command -def _encode_message(data: opamp_pb2.AgentToServer) -> bytes: +def encode_message(data: opamp_pb2.AgentToServer) -> bytes: return data.SerializeToString() -def _decode_remote_config( +def decode_remote_config( remote_config: opamp_pb2.AgentRemoteConfig, ) -> Generator[tuple[str, Mapping[str, AnyValue]]]: for ( diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/requests.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/requests.py index 3af088a5de..9e76afec10 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/requests.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/requests.py @@ -44,6 +44,6 @@ def send( except Exception: raise OpAMPException - message = messages._decode_message(response.content) + message = messages.decode_message(response.content) return message diff --git a/opamp/opentelemetry-opamp-client/tests/opamp/test_agent.py b/opamp/opentelemetry-opamp-client/tests/opamp/test_agent.py index dff25b24c2..9582151176 100644 --- a/opamp/opentelemetry-opamp-client/tests/opamp/test_agent.py +++ b/opamp/opentelemetry-opamp-client/tests/opamp/test_agent.py @@ -38,7 +38,7 @@ def test_can_start_agent(): 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 + client_mock.send.return_value = mock_message message_handler = mock.Mock() agent = OpAMPAgent( interval=30, client=client_mock, message_handler=message_handler @@ -49,7 +49,7 @@ def test_agent_start_will_send_connection_and_disconnetion_messages(): agent.stop() # one send for connection message, one for disconnect agent message - assert client_mock._send.call_count == 2 + assert client_mock.send.call_count == 2 # connection callback has been called assert agent._schedule is True # connection message response has been received @@ -92,7 +92,7 @@ def test_agent_retries_before_max_attempts(caplog): message_handler_mock = mock.Mock() client_mock = mock.Mock() connection_message = disconnection_message = server_message = mock.Mock() - client_mock._send.side_effect = [ + client_mock.send.side_effect = [ connection_message, Exception, server_message, @@ -110,7 +110,7 @@ def test_agent_retries_before_max_attempts(caplog): sleep(0.1) agent.stop() - assert client_mock._send.call_count == 4 + assert client_mock.send.call_count == 4 assert message_handler_mock.call_count == 2 @@ -119,7 +119,7 @@ def test_agent_stops_after_max_attempts(caplog): message_handler_mock = mock.Mock() client_mock = mock.Mock() connection_message = disconnection_message = mock.Mock() - client_mock._send.side_effect = [ + client_mock.send.side_effect = [ connection_message, Exception, Exception, @@ -138,7 +138,7 @@ def test_agent_stops_after_max_attempts(caplog): sleep(0.1) agent.stop() - assert client_mock._send.call_count == 4 + assert client_mock.send.call_count == 4 assert message_handler_mock.call_count == 1 diff --git a/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py b/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py index 5b47a9183f..58d8a69d95 100644 --- a/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py +++ b/opamp/opentelemetry-opamp-client/tests/opamp/test_client.py @@ -100,7 +100,7 @@ def test_client_headers_override_defaults(): headers={"User-Agent": "Custom"}, ) client._transport = mock.Mock() - client._send(b"") + client.send(b"") (send_call,) = client._transport.mock_calls assert send_call == mock.call.send( @@ -115,7 +115,7 @@ def test_client_headers_override_defaults(): def test_build_connection_message(client): - data = client._build_connection_message() + data = client.build_connection_message() message = opamp_pb2.AgentToServer() message.ParseFromString(data) @@ -142,7 +142,7 @@ def test_build_connection_message_can_serialize_attributes(): "float": 2.0, }, ) - data = client._build_connection_message() + data = client.build_connection_message() message = opamp_pb2.AgentToServer() message.ParseFromString(data) @@ -163,7 +163,7 @@ def test_build_connection_message_can_serialize_attributes(): def test_build_agent_disconnect_message(client): - data = client._build_agent_disconnect_message() + data = client.build_agent_disconnect_message() message = opamp_pb2.AgentToServer() message.ParseFromString(data) @@ -176,7 +176,7 @@ def test_build_agent_disconnect_message(client): def test_build_heartbeat_message(client): - data = client._build_heartbeat_message() + data = client.build_heartbeat_message() message = opamp_pb2.AgentToServer() message.ParseFromString(data) @@ -188,7 +188,7 @@ def test_build_heartbeat_message(client): def test_update_remote_config_status_without_previous_config(client): - remote_config_status = client._update_remote_config_status( + remote_config_status = client.update_remote_config_status( remote_config_hash=b"12345678", status=opamp_pb2.RemoteConfigStatuses_APPLIED, ) @@ -202,14 +202,14 @@ def test_update_remote_config_status_without_previous_config(client): def test_update_remote_config_status_with_same_config(client): - remote_config_status = client._update_remote_config_status( + remote_config_status = client.update_remote_config_status( remote_config_hash=b"12345678", status=opamp_pb2.RemoteConfigStatuses_APPLIED, ) assert remote_config_status is not None - remote_config_status = client._update_remote_config_status( + remote_config_status = client.update_remote_config_status( remote_config_hash=b"12345678", status=opamp_pb2.RemoteConfigStatuses_APPLIED, ) @@ -218,7 +218,7 @@ def test_update_remote_config_status_with_same_config(client): def test_update_remote_config_status_with_diffent_config(client): - remote_config_status = client._update_remote_config_status( + remote_config_status = client.update_remote_config_status( remote_config_hash=b"12345678", status=opamp_pb2.RemoteConfigStatuses_APPLIED, ) @@ -226,7 +226,7 @@ def test_update_remote_config_status_with_diffent_config(client): assert remote_config_status is not None # different status - remote_config_status = client._update_remote_config_status( + remote_config_status = client.update_remote_config_status( remote_config_hash=b"12345678", status=opamp_pb2.RemoteConfigStatuses_FAILED, ) @@ -234,7 +234,7 @@ def test_update_remote_config_status_with_diffent_config(client): assert remote_config_status is not None # different error message - remote_config_status = client._update_remote_config_status( + remote_config_status = client.update_remote_config_status( remote_config_hash=b"12345678", status=opamp_pb2.RemoteConfigStatuses_FAILED, error_message="different error message", @@ -243,7 +243,7 @@ def test_update_remote_config_status_with_diffent_config(client): assert remote_config_status is not None # different hash - remote_config_status = client._update_remote_config_status( + remote_config_status = client.update_remote_config_status( remote_config_hash=b"1234", status=opamp_pb2.RemoteConfigStatuses_FAILED, error_message="different error message", @@ -253,11 +253,11 @@ def test_update_remote_config_status_with_diffent_config(client): def test_build_remote_config_status_response_message_no_error_message(client): - remote_config_status = messages._build_remote_config_status_message( + remote_config_status = messages.build_remote_config_status_message( last_remote_config_hash=b"12345678", status=opamp_pb2.RemoteConfigStatuses_APPLIED, ) - data = client._build_remote_config_status_response_message( + data = client.build_remote_config_status_response_message( remote_config_status ) @@ -280,12 +280,12 @@ def test_build_remote_config_status_response_message_no_error_message(client): def test_build_remote_config_status_response_message_with_error_message( client, ): - remote_config_status = messages._build_remote_config_status_message( + remote_config_status = messages.build_remote_config_status_message( last_remote_config_hash=b"12345678", status=opamp_pb2.RemoteConfigStatuses_FAILED, error_message="an error message", ) - data = client._build_remote_config_status_response_message( + data = client.build_remote_config_status_response_message( remote_config_status ) @@ -308,8 +308,8 @@ def test_build_remote_config_status_response_message_with_error_message( def test_message_sequence_num_increases_in_send(client): client._transport = mock.Mock() for index in range(2): - data = client._build_heartbeat_message() - client._send(data) + data = client.build_heartbeat_message() + client.send(data) message = opamp_pb2.AgentToServer() message.ParseFromString(data) @@ -320,7 +320,7 @@ def test_message_sequence_num_increases_in_send(client): def test_send(client): client._transport = mock.Mock() - client._send(b"foo") + client.send(b"foo") (send_call,) = client._transport.mock_calls assert send_call == mock.call.send( @@ -346,7 +346,7 @@ def test_decode_remote_config(client): config.config_map["text/json"].content_type = "text/json" message = opamp_pb2.AgentRemoteConfig(config=config) - decoded = list(client._decode_remote_config(message)) + decoded = list(client.decode_remote_config(message)) assert sorted(decoded) == sorted( [ ("application/json", {"a": "config"}), @@ -362,7 +362,7 @@ def test_decode_remote_config_invalid_content_type(client): message = opamp_pb2.AgentRemoteConfig(config=config) with pytest.raises(OpAMPRemoteConfigParseException): - list(client._decode_remote_config(message)) + list(client.decode_remote_config(message)) def test_decode_remote_config_invalid_file_body(client): @@ -372,4 +372,4 @@ def test_decode_remote_config_invalid_file_body(client): message = opamp_pb2.AgentRemoteConfig(config=config) with pytest.raises(OpAMPRemoteConfigDecodeException): - list(client._decode_remote_config(message)) + list(client.decode_remote_config(message)) diff --git a/opamp/opentelemetry-opamp-client/tests/opamp/test_e2e.py b/opamp/opentelemetry-opamp-client/tests/opamp/test_e2e.py index 3884e69dd7..d023c963c0 100644 --- a/opamp/opentelemetry-opamp-client/tests/opamp/test_e2e.py +++ b/opamp/opentelemetry-opamp-client/tests/opamp/test_e2e.py @@ -41,14 +41,14 @@ def opamp_handler(agent, client, message): if not message.remote_config.config_hash: return - updated_remote_config = client._update_remote_config_status( + updated_remote_config = client.update_remote_config_status( remote_config_hash=message.remote_config.config_hash, status=opamp_pb2.RemoteConfigStatuses_APPLIED, error_message="", ) if updated_remote_config is not None: logger.debug("Updated Remote Config") - message = client._build_remote_config_status_response_message( + message = client.build_remote_config_status_response_message( updated_remote_config ) agent.send(payload=message) From 26c68e8efb575d33b1ab43b45f60cce8403c0638 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 12 Sep 2025 16:33:42 +0200 Subject: [PATCH 29/39] Add missing protobuf package installation for typecheck --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index df588b5af0..1be7354ecf 100644 --- a/tox.ini +++ b/tox.ini @@ -1083,6 +1083,7 @@ commands = deps = -c {toxinidir}/dev-requirements.txt pyright + protobuf==6.31.1 {[testenv]test_deps} {toxinidir}/opentelemetry-instrumentation {toxinidir}/util/opentelemetry-util-http From 3eb1769c00361f529b0ba89c1c1371ee35428679 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 12 Sep 2025 17:03:16 +0200 Subject: [PATCH 30/39] Fix docs generation --- docs/conf.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index cf2e9c108f..d818b33c92 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -149,7 +149,12 @@ # Sphinx does not recognize generic type TypeVars # Container supposedly were fixed, but does not work # https://github.com/sphinx-doc/sphinx/pull/3744 -nitpick_ignore = [] +nitpick_ignore = [ + ( + "py:class", + "opamp_pb2.RemoteConfigStatus", + ), +] cfg = ConfigParser() cfg.read("./nitpick-exceptions.ini") From eef2a4494e811f005ebfe4bd60a357b869f0dda0 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Tue, 30 Sep 2025 18:16:33 +0200 Subject: [PATCH 31/39] Fix pyright exclusion rule for proto Missed .pyi exclusion --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6fc1cdadb6..7f82264921 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -222,5 +222,5 @@ exclude = [ "instrumentation-genai/opentelemetry-instrumentation-weaviate/examples/**/*.py", "util/opentelemetry-util-genai/tests/**/*.py", "opamp/opentelemetry-opamp-client/tests/**/*.py", - "opamp/opentelemetry-opamp-client/src/opentelemetry/*/proto/*.py", + "opamp/opentelemetry-opamp-client/src/opentelemetry/**/proto/**", ] From 14c5aaa67920d3d31eed5fb13eb4f6e3e43cfcd9 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 1 Oct 2025 14:39:58 +0200 Subject: [PATCH 32/39] Feedback --- opamp/opentelemetry-opamp-client/MANIFEST.rst | 9 --------- .../src/opentelemetry/_opamp/__init__.py | 2 +- .../src/opentelemetry/_opamp/agent.py | 14 +++++++------- 3 files changed, 8 insertions(+), 17 deletions(-) delete mode 100644 opamp/opentelemetry-opamp-client/MANIFEST.rst diff --git a/opamp/opentelemetry-opamp-client/MANIFEST.rst b/opamp/opentelemetry-opamp-client/MANIFEST.rst deleted file mode 100644 index 2906eeef0f..0000000000 --- a/opamp/opentelemetry-opamp-client/MANIFEST.rst +++ /dev/null @@ -1,9 +0,0 @@ -graft src -graft tests -global-exclude *.pyc -global-exclude *.pyo -global-exclude __pycache__/* -include CHANGELOG.md -include MANIFEST.in -include README.rst -include LICENSE \ No newline at end of file diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py index d2773fb66e..01df5d3a60 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py @@ -16,7 +16,7 @@ OpenTelemetry Python - OpAMP client ----------------------------------- -This package provides a bunch of classes that can be used by OpenTelemetry distributions implementers +This package provides a bunch of classes that can be used by OpenTelemetry distributions implementors to implement remote config support via the `OpAMP protocol`_. The client implements the following capabilities: diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py index 525388c5f4..4ced8aed1e 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py @@ -158,7 +158,7 @@ def send( def _run_scheduler(self) -> None: """ - After me made a connection periodically enqueue “heartbeat” jobs until stop is signaled. + After we made a connection, periodically enqueue “heartbeat” jobs until stop is signaled. """ while not self._stop.wait(self._interval): if self._schedule: @@ -240,16 +240,16 @@ def stop(self) -> None: # 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") + logger.debug("Stopping OpAMPAgent: sending AgentDisconnect") payload = self._client.build_agent_disconnect_message() try: self._client.send(payload) except Exception: # pylint: disable=broad-exception-caught logger.debug( - "Stopping OpAMPClient: failed to send AgentDisconnect message" + "Stopping OpAMPAgent: failed to send AgentDisconnect message" ) - logger.debug("Stopping OpAMPClient: cancelling jobs") + logger.debug("Stopping OpAMPAgent: cancelling jobs") # Clear pending jobs while True: try: @@ -265,12 +265,12 @@ def stop(self) -> None: self._worker.join() except RuntimeError as exc: logger.warning( - "Stopping OpAMPClient: worker thread failed to join %s", exc + "Stopping OpAMPAgent: 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 + "Stopping OpAMPAgent: scheduler thread failed to join %s", exc ) - logger.debug("OpAMPClient stopped") + logger.debug("OpAMPAgent stopped") From be4ab84779477ade3691c20305f5f4516ee09215 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 1 Oct 2025 15:13:14 +0200 Subject: [PATCH 33/39] Don't flush the queue at exit --- .../src/opentelemetry/_opamp/agent.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py index 4ced8aed1e..0a604f1f3b 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py @@ -249,15 +249,7 @@ def stop(self) -> None: "Stopping OpAMPAgent: failed to send AgentDisconnect message" ) - logger.debug("Stopping OpAMPAgent: cancelling jobs") - # Clear pending jobs - while True: - try: - self._queue.get_nowait() - self._queue.task_done() - except queue.Empty: - break - + logger.debug("Stopping OpAMPAgent: signaling threads") # Signal threads to exit self._stop.set() # don't crash if the user calls stop() before start() or calls stop() multiple times From beb046231910ce305cfe3dcb80faf73f4b0e1e1e Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 1 Oct 2025 16:14:07 +0200 Subject: [PATCH 34/39] Log transport send exceptions --- .../_opamp/transport/requests.py | 6 +- ...ig_status_heartbeat_disconnection.yaml.lel | 134 ++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 opamp/opentelemetry-opamp-client/tests/opamp/cassettes/test_connection_remote_config_status_heartbeat_disconnection.yaml.lel diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/requests.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/requests.py index 9e76afec10..ac5ccaf909 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/requests.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/transport/requests.py @@ -14,6 +14,7 @@ from __future__ import annotations +import logging from typing import Mapping import requests @@ -22,6 +23,8 @@ from opentelemetry._opamp.transport.base import HttpTransport, base_headers from opentelemetry._opamp.transport.exceptions import OpAMPException +logger = logging.getLogger(__name__) + class RequestsTransport(HttpTransport): def __init__(self, session: requests.Session | None = None): @@ -41,7 +44,8 @@ def send( url, headers=headers, data=data, timeout=timeout ) response.raise_for_status() - except Exception: + except Exception as exc: + logger.error(str(exc)) raise OpAMPException message = messages.decode_message(response.content) diff --git a/opamp/opentelemetry-opamp-client/tests/opamp/cassettes/test_connection_remote_config_status_heartbeat_disconnection.yaml.lel b/opamp/opentelemetry-opamp-client/tests/opamp/cassettes/test_connection_remote_config_status_heartbeat_disconnection.yaml.lel new file mode 100644 index 0000000000..7497aaba48 --- /dev/null +++ b/opamp/opentelemetry-opamp-client/tests/opamp/cassettes/test_connection_remote_config_status_heartbeat_disconnection.yaml.lel @@ -0,0 +1,134 @@ +interactions: +- request: + body: !!binary | + ChABl89ktxVxE7d60nbzJJzzGj0KFQoMc2VydmljZS5uYW1lEgUKA2ZvbwokChtkZXBsb3ltZW50 + LmVudmlyb25tZW50Lm5hbWUSBQoDZm9vIINg + 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 | + ChABl89ktxVxE7d60nbzJJzzGmYKOgo4CgdlbGFzdGljEi0KGXsibG9nZ2luZ19sZXZlbCI6ImRl + YnVnIn0SEGFwcGxpY2F0aW9uL2pzb24SKGY3MzA5M2VjZDEyNjkzZGMxNDUxYWQ2MjdlZDA2MWJl + ZWM5ZjU1OWM4AlIA + headers: + Content-Length: + - '126' + Content-Type: + - application/x-protobuf + Date: + - Thu, 03 Jul 2025 08:26:13 GMT + status: + code: 200 + message: OK +- request: + body: !!binary | + ChABl89ktxVxE7d60nbzJJzzEAEgg2A6LAooZjczMDkzZWNkMTI2OTNkYzE0NTFhZDYyN2VkMDYx + YmVlYzlmNTU5YxAB + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '69' + 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 | + ChABl89ktxVxE7d60nbzJJzzOAJSAA== + headers: + Content-Length: + - '22' + Content-Type: + - application/x-protobuf + Date: + - Thu, 03 Jul 2025 08:26:13 GMT + status: + code: 200 + message: OK +- request: + body: !!binary | + ChABl89ktxVxE7d60nbzJJzzEAIgg2A= + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '23' + 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 | + ChABl89ktxVxE7d60nbzJJzzOAJSAA== + headers: + Content-Length: + - '22' + Content-Type: + - application/x-protobuf + Date: + - Thu, 03 Jul 2025 08:26:14 GMT + status: + code: 200 + message: OK +- request: + body: !!binary | + ChABl89ktxVxE7d60nbzJJzzEAMgg2BKAA== + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '25' + 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 | + ChABl89ktxVxE7d60nbzJJzzOAJSAA== + headers: + Content-Length: + - '22' + Content-Type: + - application/x-protobuf + Date: + - Thu, 03 Jul 2025 08:26:15 GMT + status: + code: 200 + message: OK +version: 1 From 3c1be91538d98269ffb2347c71b25eb6889564f2 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 1 Oct 2025 16:20:21 +0200 Subject: [PATCH 35/39] Update example to not assume that the config is in json format --- .../src/opentelemetry/_opamp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py index 01df5d3a60..789afc2820 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py @@ -54,7 +54,7 @@ def opamp_handler(agent: OpAMPAgent, client: OpAMPClient, message: opamp_pb2.ServerToAgent): - for config_filename, config in client.decode_remote_config(message.remote_config): + for config_filename, config in message.remote_config.config.config_map.items(): print("do something") From 5fd86b9fd2f7f32e93688bdcacd251e5ab9d06de Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 2 Oct 2025 11:51:30 +0200 Subject: [PATCH 36/39] Fix typo in exception --- .../src/opentelemetry/_opamp/messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py index df7ee95cba..cbb138c0d9 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py @@ -161,7 +161,7 @@ def decode_remote_config( 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}" + f"Failed to decode {config_file_name} with content type {config_file.content_type}: {exc}" ) yield config_file_name, config_data From 7cab3a06279cdb34b1f52fe4109195532cef00be Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 2 Oct 2025 14:45:58 +0200 Subject: [PATCH 37/39] Looks like it's implementers --- .../src/opentelemetry/_opamp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py index 789afc2820..9087d6e165 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/__init__.py @@ -16,7 +16,7 @@ OpenTelemetry Python - OpAMP client ----------------------------------- -This package provides a bunch of classes that can be used by OpenTelemetry distributions implementors +This package provides a bunch of classes that can be used by OpenTelemetry distributions implementers to implement remote config support via the `OpAMP protocol`_. The client implements the following capabilities: From e8d6b59d7691c768bc1734b7095e77a224721c8a Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 6 Oct 2025 12:21:02 +0200 Subject: [PATCH 38/39] Add timeout to stop to forward to threads join --- .../src/opentelemetry/_opamp/agent.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py index 0a604f1f3b..f76560c5db 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py @@ -233,9 +233,11 @@ def _run_worker(self) -> None: finally: self._queue.task_done() - def stop(self) -> None: + def stop(self, timeout: float | None = None) -> None: """ - Immediately cancel all in-flight and queued jobs, then join threads. + Signal server we are disconnecting and then threads to exit + + :param timeout: seconds to wait for threads to join """ # Before exiting send signal the server we are disconnecting to free our resources @@ -254,13 +256,13 @@ def stop(self) -> None: self._stop.set() # don't crash if the user calls stop() before start() or calls stop() multiple times try: - self._worker.join() + self._worker.join(timeout=timeout) except RuntimeError as exc: logger.warning( "Stopping OpAMPAgent: worker thread failed to join %s", exc ) try: - self._scheduler.join() + self._scheduler.join(timeout=timeout) except RuntimeError as exc: logger.warning( "Stopping OpAMPAgent: scheduler thread failed to join %s", exc From 9613bdeef9b6349e97e212064c7be0aa78aaa087 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 6 Oct 2025 14:39:21 +0200 Subject: [PATCH 39/39] Clarify doc --- .../src/opentelemetry/_opamp/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py index f76560c5db..d9b4cf864c 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/agent.py @@ -237,7 +237,7 @@ def stop(self, timeout: float | None = None) -> None: """ Signal server we are disconnecting and then threads to exit - :param timeout: seconds to wait for threads to join + :param timeout: seconds to wait for each thread to join """ # Before exiting send signal the server we are disconnecting to free our resources