From ff1a0dcae0074de648aea0e878184378ed7cd7f1 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Mon, 15 Dec 2025 16:38:56 +0000 Subject: [PATCH 1/4] Add remote config settings and ensure correct capabilities - Introduce `RemoteConfigSettings` to allow configuring remote configuration support in the OpAMP client. - Update capability advertisement logic to include `AcceptsRemoteConfig` when enabled. - Add and update tests to verify capability flags and heartbeat behavior. - Update unshipped public API. --- .../.publicApi/PublicAPI.Unshipped.txt | 6 ++ src/OpenTelemetry.OpAmp.Client/CHANGELOG.md | 3 + .../Internal/FrameBuilder.cs | 17 +++- .../Settings/OpAmpClientSettings.cs | 5 ++ .../Settings/RemoteConfigSettings.cs | 19 +++++ .../HeartbeatServiceTests.cs | 2 + .../OpAmpClientTests.cs | 78 +++++++++++++++++++ .../Tools/OpAmpFakeHttpServer.cs | 2 +- 8 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 src/OpenTelemetry.OpAmp.Client/Settings/RemoteConfigSettings.cs diff --git a/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt index c6ffd9adba..175e983429 100644 --- a/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt @@ -56,8 +56,14 @@ OpenTelemetry.OpAmp.Client.Settings.OpAmpClientSettings.Identification.set -> vo OpenTelemetry.OpAmp.Client.Settings.OpAmpClientSettings.InstanceUid.get -> System.Guid OpenTelemetry.OpAmp.Client.Settings.OpAmpClientSettings.InstanceUid.set -> void OpenTelemetry.OpAmp.Client.Settings.OpAmpClientSettings.OpAmpClientSettings() -> void +OpenTelemetry.OpAmp.Client.Settings.OpAmpClientSettings.RemoteConfiguration.get -> OpenTelemetry.OpAmp.Client.Settings.RemoteConfigSettings! +OpenTelemetry.OpAmp.Client.Settings.OpAmpClientSettings.RemoteConfiguration.set -> void OpenTelemetry.OpAmp.Client.Settings.OpAmpClientSettings.ServerUrl.get -> System.Uri! OpenTelemetry.OpAmp.Client.Settings.OpAmpClientSettings.ServerUrl.set -> void +OpenTelemetry.OpAmp.Client.Settings.RemoteConfigSettings +OpenTelemetry.OpAmp.Client.Settings.RemoteConfigSettings.AcceptsRemoteConfig.get -> bool +OpenTelemetry.OpAmp.Client.Settings.RemoteConfigSettings.AcceptsRemoteConfig.set -> void +OpenTelemetry.OpAmp.Client.Settings.RemoteConfigSettings.RemoteConfigSettings() -> void override OpenTelemetry.OpAmp.Client.Settings.AnyValueUnion.Equals(object? obj) -> bool override OpenTelemetry.OpAmp.Client.Settings.AnyValueUnion.GetHashCode() -> int static OpenTelemetry.OpAmp.Client.Settings.AnyValueUnion.operator !=(OpenTelemetry.OpAmp.Client.Settings.AnyValueUnion left, OpenTelemetry.OpAmp.Client.Settings.AnyValueUnion right) -> bool diff --git a/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md b/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md index b2e9297081..e6e41ef58a 100644 --- a/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md +++ b/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md @@ -15,6 +15,9 @@ * Expose public `RemoteConfigMessage`. ([#3614](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3614)) +* Add settings for remote configuration and update advertised capabilities. + (TODO) + ## 0.1.0-alpha.3 Released 2025-Nov-13 diff --git a/src/OpenTelemetry.OpAmp.Client/Internal/FrameBuilder.cs b/src/OpenTelemetry.OpAmp.Client/Internal/FrameBuilder.cs index e4d535f121..d02e1857d4 100644 --- a/src/OpenTelemetry.OpAmp.Client/Internal/FrameBuilder.cs +++ b/src/OpenTelemetry.OpAmp.Client/Internal/FrameBuilder.cs @@ -132,9 +132,20 @@ IFrameBuilder IFrameBuilder.AddCapabilities() this.EnsureInitialized(); // TODO: Update the actual capabilities when features are implemented. - this.currentMessage.Capabilities = (ulong)(AgentCapabilities.ReportsStatus - | AgentCapabilities.ReportsHealth - | AgentCapabilities.ReportsHeartbeat); + + var capabilities = AgentCapabilities.ReportsStatus; + + if (this.settings.Heartbeat.IsEnabled) + { + capabilities |= AgentCapabilities.ReportsHeartbeat | AgentCapabilities.ReportsHealth; + } + + if (this.settings.RemoteConfiguration.AcceptsRemoteConfig) + { + capabilities |= AgentCapabilities.AcceptsRemoteConfig; + } + + this.currentMessage.Capabilities = (ulong)capabilities; return this; } diff --git a/src/OpenTelemetry.OpAmp.Client/Settings/OpAmpClientSettings.cs b/src/OpenTelemetry.OpAmp.Client/Settings/OpAmpClientSettings.cs index ab0178239b..276395d64e 100644 --- a/src/OpenTelemetry.OpAmp.Client/Settings/OpAmpClientSettings.cs +++ b/src/OpenTelemetry.OpAmp.Client/Settings/OpAmpClientSettings.cs @@ -100,6 +100,11 @@ public Uri ServerUrl /// public HeartbeatSettings Heartbeat { get; set; } = new(); + /// + /// Gets or sets the remote configuration settings. + /// + public RemoteConfigSettings RemoteConfiguration { get; set; } = new(); + /// /// Gets or sets the factory function called to create the instance that will be used at runtime to diff --git a/src/OpenTelemetry.OpAmp.Client/Settings/RemoteConfigSettings.cs b/src/OpenTelemetry.OpAmp.Client/Settings/RemoteConfigSettings.cs new file mode 100644 index 0000000000..4e0f40fbdb --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Settings/RemoteConfigSettings.cs @@ -0,0 +1,19 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.OpAmp.Client.Settings; + +/// +/// Configuration settings for the remote configuration capability of the client. +/// +public sealed class RemoteConfigSettings +{ + /// + /// Gets or sets a value indicating whether the client accepts remote configuration. + /// + /// + /// true if remote configuration is accepted and should be sent by the server; otherwise, false. + /// Default is false. + /// + public bool AcceptsRemoteConfig { get; set; } +} diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/HeartbeatServiceTests.cs b/test/OpenTelemetry.OpAmp.Client.Tests/HeartbeatServiceTests.cs index bf6d7a623b..9734930954 100644 --- a/test/OpenTelemetry.OpAmp.Client.Tests/HeartbeatServiceTests.cs +++ b/test/OpenTelemetry.OpAmp.Client.Tests/HeartbeatServiceTests.cs @@ -1,10 +1,12 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using OpAmp.Proto.V1; using OpenTelemetry.OpAmp.Client.Internal; using OpenTelemetry.OpAmp.Client.Internal.Services.Heartbeat; using OpenTelemetry.OpAmp.Client.Settings; using OpenTelemetry.OpAmp.Client.Tests.Mocks; +using OpenTelemetry.OpAmp.Client.Tests.Tools; using Xunit; namespace OpenTelemetry.OpAmp.Client.Tests; diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/OpAmpClientTests.cs b/test/OpenTelemetry.OpAmp.Client.Tests/OpAmpClientTests.cs index 73e7772920..076aed1703 100644 --- a/test/OpenTelemetry.OpAmp.Client.Tests/OpAmpClientTests.cs +++ b/test/OpenTelemetry.OpAmp.Client.Tests/OpAmpClientTests.cs @@ -1,7 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using OpAmp.Proto.V1; using OpenTelemetry.OpAmp.Client.Internal.Services.Heartbeat; +using OpenTelemetry.OpAmp.Client.Settings; using OpenTelemetry.OpAmp.Client.Tests.Mocks; using OpenTelemetry.OpAmp.Client.Tests.Tools; using Xunit; @@ -74,4 +76,80 @@ static ulong GetCurrentTimeInNanoseconds() return (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000; // Convert to nanoseconds } } + + [Fact] + public async Task DoesNotEmitHeartbeat_WhenDisabled() + { + using var opAmpServer = new OpAmpFakeHttpServer(false); + var opAmpEndpoint = opAmpServer.Endpoint; + + using var mockListener = new MockListener(); + using var client = new OpAmpClient(o => + { + o.ServerUrl = opAmpEndpoint; + o.Heartbeat.IsEnabled = false; + }); + client.Subscribe(mockListener); + + await client.StartAsync(); + + mockListener.WaitForMessages(TimeSpan.FromSeconds(2)); + + var frames = opAmpServer.GetFrames(); + + // Only the identification message should be received + Assert.Single(frames); + Assert.Single(mockListener.Messages); + + await client.StopAsync(); + } + + [Theory] + [ClassData(typeof(CapabilityTestData))] + internal async Task SendsExpectedCapabilities( + Action configure, + IEnumerable expectedCapabilities, + IEnumerable notExpectedCapabilities) + { + using var opAmpServer = new OpAmpFakeHttpServer(false); + var opAmpEndpoint = opAmpServer.Endpoint; + configure += o => o.ServerUrl = opAmpEndpoint; + + using var mockListener = new MockListener(); + using var client = new OpAmpClient(configure); + client.Subscribe(mockListener); + + await client.StartAsync(); + + var frames = opAmpServer.GetFrames(); + + Assert.True(frames.Count >= 1, "Expecting at least one server frame."); + + var identificationFrame = frames[0]; + var capabilities = (AgentCapabilities)identificationFrame.Capabilities; + + foreach (var expectedCapability in expectedCapabilities) + { + Assert.True(capabilities.HasFlag(expectedCapability), $"Expected capabilities to include {expectedCapability}."); + } + + foreach (var notExpectedCapability in notExpectedCapabilities) + { + Assert.False(capabilities.HasFlag(notExpectedCapability), $"Expected capabilities not to include {notExpectedCapability}."); + } + + await client.StopAsync(); + } + + internal class CapabilityTestData + : TheoryData, IEnumerable, IEnumerable> + { + public CapabilityTestData() + { + this.Add(o => o.Heartbeat.IsEnabled = false, [], [AgentCapabilities.ReportsHeartbeat, AgentCapabilities.ReportsHealth]); + this.Add(o => o.Heartbeat.IsEnabled = true, [AgentCapabilities.ReportsHeartbeat, AgentCapabilities.ReportsHealth], []); + this.Add(o => o.RemoteConfiguration.AcceptsRemoteConfig = true, [AgentCapabilities.AcceptsRemoteConfig], []); + this.Add(o => o.RemoteConfiguration.AcceptsRemoteConfig = false, [], [AgentCapabilities.AcceptsRemoteConfig]); + } + } } diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/Tools/OpAmpFakeHttpServer.cs b/test/OpenTelemetry.OpAmp.Client.Tests/Tools/OpAmpFakeHttpServer.cs index a90a0da868..2c0b519309 100644 --- a/test/OpenTelemetry.OpAmp.Client.Tests/Tools/OpAmpFakeHttpServer.cs +++ b/test/OpenTelemetry.OpAmp.Client.Tests/Tools/OpAmpFakeHttpServer.cs @@ -45,7 +45,7 @@ public OpAmpFakeHttpServer(bool useSmallPackets) public Uri Endpoint { get; } - public IReadOnlyCollection GetFrames() + public IReadOnlyList GetFrames() { return this.frames.ToArray(); } From 94a274bed325ff73109503d6a36463e134bd204d Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Tue, 16 Dec 2025 10:10:35 +0000 Subject: [PATCH 2/4] Update changelog --- src/OpenTelemetry.OpAmp.Client/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md b/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md index e6e41ef58a..01f9dd0fd7 100644 --- a/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md +++ b/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md @@ -16,7 +16,7 @@ ([#3614](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3614)) * Add settings for remote configuration and update advertised capabilities. - (TODO) + ([#3618](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3618)) ## 0.1.0-alpha.3 From ae34a2ec271241913e7a21066f59967358104923 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Tue, 16 Dec 2025 10:26:55 +0000 Subject: [PATCH 3/4] Fix formatting --- test/OpenTelemetry.OpAmp.Client.Tests/HeartbeatServiceTests.cs | 2 -- .../OpenTelemetry.OpAmp.Client.Tests/PlainHttpTransportTests.cs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/HeartbeatServiceTests.cs b/test/OpenTelemetry.OpAmp.Client.Tests/HeartbeatServiceTests.cs index 9734930954..bf6d7a623b 100644 --- a/test/OpenTelemetry.OpAmp.Client.Tests/HeartbeatServiceTests.cs +++ b/test/OpenTelemetry.OpAmp.Client.Tests/HeartbeatServiceTests.cs @@ -1,12 +1,10 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using OpAmp.Proto.V1; using OpenTelemetry.OpAmp.Client.Internal; using OpenTelemetry.OpAmp.Client.Internal.Services.Heartbeat; using OpenTelemetry.OpAmp.Client.Settings; using OpenTelemetry.OpAmp.Client.Tests.Mocks; -using OpenTelemetry.OpAmp.Client.Tests.Tools; using Xunit; namespace OpenTelemetry.OpAmp.Client.Tests; diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/PlainHttpTransportTests.cs b/test/OpenTelemetry.OpAmp.Client.Tests/PlainHttpTransportTests.cs index 3c04c5c62f..22c9db42a3 100644 --- a/test/OpenTelemetry.OpAmp.Client.Tests/PlainHttpTransportTests.cs +++ b/test/OpenTelemetry.OpAmp.Client.Tests/PlainHttpTransportTests.cs @@ -43,7 +43,7 @@ public async Task PlainHttpTransport_SendReceiveCommunication(bool useSmallPacke var receivedTextData = clientReceivedFrames.First().CustomMessage.Data.ToStringUtf8(); Assert.Single(serverReceivedFrames); - Assert.Equal(mockFrame.Uid, serverReceivedFrames.First().InstanceUid); + Assert.Equal(mockFrame.Uid, serverReceivedFrames[0].InstanceUid); Assert.Single(clientReceivedFrames); Assert.StartsWith("This is a mock server frame for testing purposes.", receivedTextData); From 50342a6f8f77b20afa467eba4506240a890838c2 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Tue, 16 Dec 2025 12:14:51 +0000 Subject: [PATCH 4/4] PR feedback --- .../PlainHttpTransportTests.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/PlainHttpTransportTests.cs b/test/OpenTelemetry.OpAmp.Client.Tests/PlainHttpTransportTests.cs index 22c9db42a3..f3fc4dd71c 100644 --- a/test/OpenTelemetry.OpAmp.Client.Tests/PlainHttpTransportTests.cs +++ b/test/OpenTelemetry.OpAmp.Client.Tests/PlainHttpTransportTests.cs @@ -42,8 +42,8 @@ public async Task PlainHttpTransport_SendReceiveCommunication(bool useSmallPacke var clientReceivedFrames = mockListener.Messages; var receivedTextData = clientReceivedFrames.First().CustomMessage.Data.ToStringUtf8(); - Assert.Single(serverReceivedFrames); - Assert.Equal(mockFrame.Uid, serverReceivedFrames[0].InstanceUid); + var frame = Assert.Single(serverReceivedFrames); + Assert.Equal(mockFrame.Uid, frame.InstanceUid); Assert.Single(clientReceivedFrames); Assert.StartsWith("This is a mock server frame for testing purposes.", receivedTextData); @@ -71,7 +71,6 @@ public async Task PlainHttpTransport_UsesConfiguredHttpClientFactory() frameProcessor.Subscribe(mockListener); var httpTransport = new PlainHttpTransport(settings, frameProcessor); - var mockFrame = FrameGenerator.GenerateMockAgentFrame(false); // Act