Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions opamp/opentelemetry-opamp-client/.changelog/4897.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-opamp-client`: allow configuring agent capabilities
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"User-Agent": "OTel-OpAMP-Python/" + __version__,
}

_HANDLED_CAPABILITIES = (
_DEFAULT_CAPABILITIES = (
opamp_pb2.AgentCapabilities.AgentCapabilities_ReportsStatus
| opamp_pb2.AgentCapabilities.AgentCapabilities_ReportsHeartbeat
| opamp_pb2.AgentCapabilities.AgentCapabilities_AcceptsRemoteConfig
Expand All @@ -52,12 +52,21 @@ def __init__(
timeout_millis: int = _DEFAULT_OPAMP_TIMEOUT_MS,
agent_identifying_attributes: Mapping[str, AnyValue],
agent_non_identifying_attributes: Mapping[str, AnyValue] | None = None,
capabilities: int = _DEFAULT_CAPABILITIES,
transport: HttpTransport | None = None,
# this matches requests but can be mapped to other http libraries APIs
tls_certificate: str | bool = True,
tls_client_certificate: str | None = None,
tls_client_key: str | None = None,
):
# ReportsStatus is required by the OpAMP specification:
# https://opentelemetry.io/docs/specs/opamp/#agenttoservercapabilities
if not capabilities & (
opamp_pb2.AgentCapabilities.AgentCapabilities_ReportsStatus
):
raise ValueError("OpAMP capabilities must include ReportsStatus")

self._capabilities = capabilities
self._timeout_millis = timeout_millis
self._transport = (
RequestsTransport() if transport is None else transport
Expand All @@ -83,7 +92,7 @@ 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,
capabilities=self._capabilities,
)
data = messages.encode_message(message)
return data
Expand All @@ -92,7 +101,7 @@ def build_heartbeat_message(self) -> bytes:
message = messages.build_heartbeat_message(
instance_uid=self._instance_uid,
sequence_num=self._sequence_num,
capabilities=_HANDLED_CAPABILITIES,
capabilities=self._capabilities,
)
data = messages.encode_message(message)
return data
Expand Down Expand Up @@ -143,7 +152,7 @@ def build_remote_config_status_response_message(
message = messages.build_remote_config_status_response_message(
instance_uid=self._instance_uid,
sequence_num=self._sequence_num,
capabilities=_HANDLED_CAPABILITIES,
capabilities=self._capabilities,
remote_config_status=remote_config_status,
)
data = messages.encode_message(message)
Expand All @@ -156,7 +165,7 @@ def build_full_state_message(self) -> bytes:
remote_config_status=self._remote_config_status,
sequence_num=self._sequence_num,
effective_config=self._effective_config,
capabilities=_HANDLED_CAPABILITIES,
capabilities=self._capabilities,
)
data = messages.encode_message(message)
return data
Expand Down
113 changes: 105 additions & 8 deletions opamp/opentelemetry-opamp-client/tests/opamp/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import pytest

from opentelemetry._opamp import messages
from opentelemetry._opamp.client import _HANDLED_CAPABILITIES, OpAMPClient
from opentelemetry._opamp.client import _DEFAULT_CAPABILITIES, OpAMPClient
from opentelemetry._opamp.exceptions import (
OpAMPRemoteConfigDecodeException,
OpAMPRemoteConfigParseException,
Expand All @@ -25,6 +25,12 @@
from opentelemetry._opamp.transport.requests import RequestsTransport
from opentelemetry._opamp.version import __version__

_EFFECTIVE_CONFIG_CAPABILITIES = (
opamp_pb2.AgentCapabilities_ReportsStatus
| opamp_pb2.AgentCapabilities_ReportsEffectiveConfig
| opamp_pb2.AgentCapabilities_ReportsHeartbeat
)


@pytest.fixture(name="client")
def client_fixture():
Expand All @@ -48,6 +54,7 @@ def test_can_instantiate_opamp_client_with_defaults():
assert client._tls_client_key is None
assert client._timeout_millis == 1_000
assert client._sequence_num == 0
assert client._capabilities == _DEFAULT_CAPABILITIES
assert isinstance(client._instance_uid, bytes)
assert isinstance(client._agent_description, opamp_pb2.AgentDescription)
assert client._agent_description.identifying_attributes == [
Expand All @@ -56,6 +63,25 @@ def test_can_instantiate_opamp_client_with_defaults():
assert client._agent_description.non_identifying_attributes == []


def test_default_capabilities_remain_unchanged():
assert _DEFAULT_CAPABILITIES == (
opamp_pb2.AgentCapabilities_ReportsStatus
| opamp_pb2.AgentCapabilities_ReportsHeartbeat
| opamp_pb2.AgentCapabilities_AcceptsRemoteConfig
| opamp_pb2.AgentCapabilities_ReportsRemoteConfig
| opamp_pb2.AgentCapabilities_ReportsEffectiveConfig
)


def test_rejects_capabilities_without_reports_status():
with pytest.raises(ValueError, match="ReportsStatus"):
OpAMPClient(
endpoint="url",
agent_identifying_attributes={"foo": "bar"},
capabilities=opamp_pb2.AgentCapabilities_ReportsHeartbeat,
)


def test_can_instantiate_opamp_client_all_params():
transport = RequestsTransport()
client = OpAMPClient(
Expand Down Expand Up @@ -145,7 +171,7 @@ def test_can_serialize_agent_identifying_attributes():
PB2KeyValue(key="float", value=PB2AnyValue(double_value=2.0)),
]
assert message.agent_description.non_identifying_attributes == []
assert message.capabilities == _HANDLED_CAPABILITIES
assert message.capabilities == _DEFAULT_CAPABILITIES


def test_build_agent_disconnect_message(client):
Expand All @@ -158,7 +184,7 @@ def test_build_agent_disconnect_message(client):
assert message.instance_uid == client._instance_uid
assert message.sequence_num == 0
assert message.agent_disconnect == opamp_pb2.AgentDisconnect()
assert message.capabilities == _HANDLED_CAPABILITIES
assert message.capabilities == _DEFAULT_CAPABILITIES


def test_build_heartbeat_message(client):
Expand All @@ -170,7 +196,58 @@ def test_build_heartbeat_message(client):
assert message
assert message.instance_uid == client._instance_uid
assert message.sequence_num == 0
assert message.capabilities == _HANDLED_CAPABILITIES
assert message.capabilities == _DEFAULT_CAPABILITIES


@pytest.mark.parametrize(
"builder_name",
[
"build_full_state_message",
"build_heartbeat_message",
"build_agent_disconnect_message",
],
)
def test_message_uses_custom_capabilities(builder_name):
client = OpAMPClient(
endpoint="url",
agent_identifying_attributes={"foo": "bar"},
capabilities=_EFFECTIVE_CONFIG_CAPABILITIES,
)

data = getattr(client, builder_name)()
message = opamp_pb2.AgentToServer()
message.ParseFromString(data)

assert message.capabilities == _EFFECTIVE_CONFIG_CAPABILITIES
assert not (
message.capabilities & opamp_pb2.AgentCapabilities_AcceptsRemoteConfig
)
assert not (
message.capabilities & opamp_pb2.AgentCapabilities_ReportsRemoteConfig
)
assert not (
message.capabilities & opamp_pb2.AgentCapabilities_ReportsHealth
)


def test_clients_use_capabilities_independently():
default_client = OpAMPClient(
endpoint="url",
agent_identifying_attributes={"foo": "bar"},
)
custom_client = OpAMPClient(
endpoint="url",
agent_identifying_attributes={"foo": "bar"},
capabilities=_EFFECTIVE_CONFIG_CAPABILITIES,
)

default_message = opamp_pb2.AgentToServer()
default_message.ParseFromString(default_client.build_full_state_message())
custom_message = opamp_pb2.AgentToServer()
custom_message.ParseFromString(custom_client.build_full_state_message())

assert default_message.capabilities == _DEFAULT_CAPABILITIES
assert custom_message.capabilities == _EFFECTIVE_CONFIG_CAPABILITIES


def test_update_remote_config_status_without_previous_config(client):
Expand Down Expand Up @@ -253,7 +330,7 @@ def test_build_remote_config_status_response_message_no_error_message(client):
assert message
assert message.instance_uid == client._instance_uid
assert message.sequence_num == 0
assert message.capabilities == _HANDLED_CAPABILITIES
assert message.capabilities == _DEFAULT_CAPABILITIES
assert message.remote_config_status
assert message.remote_config_status.last_remote_config_hash == b"12345678"
assert (
Expand Down Expand Up @@ -281,7 +358,7 @@ def test_build_remote_config_status_response_message_with_error_message(
assert message
assert message.instance_uid == client._instance_uid
assert message.sequence_num == 0
assert message.capabilities == _HANDLED_CAPABILITIES
assert message.capabilities == _DEFAULT_CAPABILITIES
assert message.remote_config_status
assert message.remote_config_status.last_remote_config_hash == b"12345678"
assert (
Expand All @@ -291,6 +368,26 @@ def test_build_remote_config_status_response_message_with_error_message(
assert message.remote_config_status.error_message == "an error message"


def test_remote_config_status_response_uses_custom_capabilities():
client = OpAMPClient(
endpoint="url",
agent_identifying_attributes={"foo": "bar"},
capabilities=_EFFECTIVE_CONFIG_CAPABILITIES,
)
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.capabilities == _EFFECTIVE_CONFIG_CAPABILITIES


def test_update_effective_config_json_content_type(client):
config = {"filename": {"a": "config"}}
client.update_effective_config(config, content_type="application/json")
Expand Down Expand Up @@ -386,7 +483,7 @@ def test_build_full_state_message(client):
assert message
assert message.instance_uid == client._instance_uid
assert message.sequence_num == 0
assert message.capabilities == _HANDLED_CAPABILITIES
assert message.capabilities == _DEFAULT_CAPABILITIES
assert message.agent_description.identifying_attributes == [
PB2KeyValue(key="foo", value=PB2AnyValue(string_value="bar")),
]
Expand All @@ -412,7 +509,7 @@ def test_build_full_state_message_no_config(client):
assert message
assert message.instance_uid == client._instance_uid
assert message.sequence_num == 0
assert message.capabilities == _HANDLED_CAPABILITIES
assert message.capabilities == _DEFAULT_CAPABILITIES
assert message.agent_description.identifying_attributes == [
PB2KeyValue(key="foo", value=PB2AnyValue(string_value="bar")),
]
Expand Down
Loading