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..01f9dd0fd7 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. + ([#3618](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3618)) + ## 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/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/PlainHttpTransportTests.cs b/test/OpenTelemetry.OpAmp.Client.Tests/PlainHttpTransportTests.cs index 3c04c5c62f..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.First().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 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(); }