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();
}