From ece6bf3ad93d9f2fe6826e323ef93a3684c1ac94 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 11 Jul 2025 16:45:38 +0200 Subject: [PATCH 01/28] 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 379735d408..beba21c433 100644 --- a/tox.ini +++ b/tox.ini @@ -423,6 +423,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 @@ -717,6 +723,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 @@ -948,6 +959,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] @@ -1065,6 +1079,7 @@ deps = {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-google-genai[instruments] {toxinidir}/instrumentation/opentelemetry-instrumentation-aiokafka[instruments] {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncclick[instruments] + {toxinidir}/opamp/opentelemetry-opamp-client[instruments] commands = pyright From c161eff5a56ec46d1ade8a700359c2a5e9739651 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 30 Jul 2025 18:41:49 +0200 Subject: [PATCH 02/28] Add some docs and hook it into the system Still not building content --- docs/conf.py | 22 ++++-- 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, 117 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 e7a7553144..743026c997 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,17 +56,24 @@ 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)) ] + +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 @@ -74,6 +81,7 @@ + sdk_ext_dirs + prop_dirs + resource_dirs + + opamp_dirs ) # -- Project information ----------------------------------------------------- @@ -115,7 +123,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 891b1b1f84e8c19622bd3bd1092801ef054a1ea5 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 30 Jul 2025 18:58:05 +0200 Subject: [PATCH 03/28] 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 a5325e78980ef0b896092c9a45f91205d1339599 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 30 Jul 2025 20:59:22 +0200 Subject: [PATCH 04/28] 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 cc246fe221..2758d6855b 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -44,6 +44,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 a80fb026dad1ead2ebefeeaa47d253fbb7acdddf Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 30 Jul 2025 21:13:21 +0200 Subject: [PATCH 05/28] 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 dc138d47fa10ba29d2b47a1ab3922254d251e52e Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 30 Jul 2025 21:16:33 +0200 Subject: [PATCH 06/28] 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 fbbaaec6a91839384410833d2f3e5f585f693695 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 30 Jul 2025 21:16:38 +0200 Subject: [PATCH 07/28] 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 743026c997..c788324dc5 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 bba2a33c8a878107cd9af531687adf994fa80b55 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 30 Jul 2025 21:17:56 +0200 Subject: [PATCH 08/28] 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 33320b5ef5..a822e876f1 100644 --- a/.github/workflows/core_contrib_test_0.yml +++ b/.github/workflows/core_contrib_test_0.yml @@ -2962,3 +2962,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 9875fed3f4..ebfb8992ce 100644 --- a/.github/workflows/lint_0.yml +++ b/.github/workflows/lint_0.yml @@ -1285,3 +1285,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 f352a83d53..297670cc0b 100644 --- a/.github/workflows/test_2.yml +++ b/.github/workflows/test_2.yml @@ -1076,3 +1076,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 f5d2a0f0d796190cf749ea859e338f6f27a4cf5b Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 1 Aug 2025 16:46:48 +0200 Subject: [PATCH 09/28] 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 beba21c433..6c149a770b 100644 --- a/tox.ini +++ b/tox.ini @@ -723,10 +723,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 @@ -960,7 +960,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 4be53c6558e40acb675cadd0aab8f1fc27978381 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 1 Aug 2025 16:51:40 +0200 Subject: [PATCH 10/28] 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 de63e15e01650499e37487fa4eedb96ebec1799a 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/28] 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 a82fb3ff6016d2bce4898e338df54b5b6738b824 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 1 Aug 2025 16:54:51 +0200 Subject: [PATCH 12/28] 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 27e4824998069056a852c2886ac76967899d5f25 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 1 Aug 2025 17:03:16 +0200 Subject: [PATCH 13/28] 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 6862d213c80569f60ff981a74086a8f53573527a Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 1 Aug 2025 17:08:21 +0200 Subject: [PATCH 14/28] 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 6c149a770b..e9e13ecf99 100644 --- a/tox.ini +++ b/tox.ini @@ -723,8 +723,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 77e8f6823847e2829c5bd8eae319f0ea4ee92990 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 25 Aug 2025 11:26:09 +0200 Subject: [PATCH 15/28] 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 a822e876f1..e82b22c26f 100644 --- a/.github/workflows/core_contrib_test_0.yml +++ b/.github/workflows/core_contrib_test_0.yml @@ -2963,8 +2963,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: @@ -2991,4 +2991,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 297670cc0b..58dabf3fa9 100644 --- a/.github/workflows/test_2.yml +++ b/.github/workflows/test_2.yml @@ -1077,8 +1077,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: @@ -1094,10 +1094,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: @@ -1113,10 +1151,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: @@ -1132,10 +1189,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: @@ -1151,10 +1227,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: @@ -1170,4 +1265,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 e9e13ecf99..83acbc3207 100644 --- a/tox.ini +++ b/tox.ini @@ -424,7 +424,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 b5eafd8467436abd027059fee754dae93c117ddb Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 25 Aug 2025 11:41:05 +0200 Subject: [PATCH 16/28] 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 e88639d1c4616f8cdad8132d1c88ab3adebde0ae Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 25 Aug 2025 11:58:10 +0200 Subject: [PATCH 17/28] 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 d7a4b51c06b615767d1b1a9ee5de5d7df45521da Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 25 Aug 2025 12:14:59 +0200 Subject: [PATCH 18/28] 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 28e52c904e..064f93a4ec 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.396 sphinx==7.1.2 From 35745c66d9a02dd17d50a7aa8009a0e20eacba53 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 25 Aug 2025 12:48:24 +0200 Subject: [PATCH 19/28] 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 c9a85af9e9bec32185001ddd27bb2f1a9b25d114 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 11 Sep 2025 11:26:55 +0200 Subject: [PATCH 20/28] 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 9e98b6c8f4748e4b87baaf6ea8585cc782d89052 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 11 Sep 2025 11:42:19 +0200 Subject: [PATCH 21/28] 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 d2508eef4c217591644a2c889a0e523a19917b63 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 11 Sep 2025 11:47:55 +0200 Subject: [PATCH 22/28] 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 064f93a4ec..28e52c904e 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.396 sphinx==7.1.2 From 8e63f7df73c7926fd61e5c9b87ed81fa7c51ee67 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 11 Sep 2025 12:02:23 +0200 Subject: [PATCH 23/28] 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 8670a2e3ce8bf6636fdac02199b93f7191266ef7 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 12 Sep 2025 11:09:59 +0200 Subject: [PATCH 24/28] Try to typecheck opamp client --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 6397c0f4da..de80e19b5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -203,6 +203,7 @@ include = [ "instrumentation-genai/opentelemetry-instrumentation-langchain", "instrumentation-genai/opentelemetry-instrumentation-weaviate", "util/opentelemetry-util-genai", + "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. @@ -217,4 +218,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 d8156c7fa4bea49c6c0e1ffdd23d58b7933fd1c2 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 12 Sep 2025 11:10:22 +0200 Subject: [PATCH 25/28] 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 ec639450553e7b68e088c011a89dfba973da75a2 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 12 Sep 2025 11:11:23 +0200 Subject: [PATCH 26/28] 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 cf479f2de0c5a88ae79a46146fbf508f3d8ecc31 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 12 Sep 2025 11:50:36 +0200 Subject: [PATCH 27/28] 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 efe76642752c5f156a447095cd243d59d8757b39 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Fri, 12 Sep 2025 13:30:24 +0200 Subject: [PATCH 28/28] Do not use underscore on private modules --- .../src/opentelemetry/_opamp/client.py | 43 ++++++++----------- .../src/opentelemetry/_opamp/messages.py | 19 ++++---- .../_opamp/transport/requests.py | 2 +- 3 files changed, 27 insertions(+), 37 deletions(-) diff --git a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py index ab1dc48b03..f959235324 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/client.py @@ -65,15 +65,13 @@ def __init__( transport: HttpTransport | None = None, ): self._timeout_millis = timeout_millis - self._transport = ( - RequestsTransport() if transport is None else transport - ) + self._transport = RequestsTransport() if transport is None else transport self._endpoint = endpoint 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, ) @@ -82,31 +80,31 @@ def __init__( self._remote_config_status: opamp_pb2.RemoteConfigStatus | None = None def _build_connection_message(self) -> bytes: - message = messages._build_presentation_message( + 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( + 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( + 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( @@ -117,8 +115,7 @@ def _update_remote_config_status( ) -> 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.last_remote_config_hash != remote_config_hash or self._remote_config_status.status != status or self._remote_config_status.error_message != error_message ) @@ -128,27 +125,23 @@ def _update_remote_config_status( "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, - ) + 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( + 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) + data = messages.encode_message(message) return data def _send(self, data: bytes): @@ -169,7 +162,5 @@ def _send(self, data: bytes): 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 - ): + 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..2fd5815565 100644 --- a/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py +++ b/opamp/opentelemetry-opamp-client/src/opentelemetry/_opamp/messages.py @@ -14,7 +14,6 @@ # 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 +34,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 +64,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 +80,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 +95,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 +106,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 +118,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 +130,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 +145,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