Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.ConfigHash.get -> System
OpenTelemetry.OpAmp.Client.OpAmpClient
OpenTelemetry.OpAmp.Client.OpAmpClient.Dispose() -> void
OpenTelemetry.OpAmp.Client.OpAmpClient.OpAmpClient(System.Action<OpenTelemetry.OpAmp.Client.Settings.OpAmpClientSettings!>? configure = null) -> void
OpenTelemetry.OpAmp.Client.OpAmpClient.SendCustomCapabilitiesAsync(System.Collections.Generic.IEnumerable<string!>! capabilities, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
OpenTelemetry.OpAmp.Client.OpAmpClient.SendCustomMessageAsync(string! capability, string! type, System.ReadOnlyMemory<byte> data, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
OpenTelemetry.OpAmp.Client.OpAmpClient.SendEffectiveConfigAsync(System.Collections.Generic.IEnumerable<OpenTelemetry.OpAmp.Client.Messages.EffectiveConfigFile!>! files, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
OpenTelemetry.OpAmp.Client.OpAmpClient.StartAsync(System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
OpenTelemetry.OpAmp.Client.OpAmpClient.StopAsync(System.Threading.CancellationToken token = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Expand Down
3 changes: 3 additions & 0 deletions src/OpenTelemetry.OpAmp.Client/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
* Add agent effective config reporting.
([#3716](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3716))

* Add ability to send custom messages.
([#3809](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3809))

## 0.1.0-alpha.4

Released 2026-Jan-14
Expand Down
24 changes: 24 additions & 0 deletions src/OpenTelemetry.OpAmp.Client/Internal/FrameBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,30 @@ IFrameBuilder IFrameBuilder.AddCapabilities()
return this;
}

IFrameBuilder IFrameBuilder.AddCustomCapabilities(IEnumerable<string> capabilities)
{
this.EnsureInitialized();

this.currentMessage.CustomCapabilities = new CustomCapabilities();
this.currentMessage.CustomCapabilities.Capabilities.Add(capabilities);

return this;
}

IFrameBuilder IFrameBuilder.AddCustomMessage(string capability, string type, ReadOnlyMemory<byte> data)
{
this.EnsureInitialized();

this.currentMessage.CustomMessage = new CustomMessage
{
Capability = capability,
Type = type,
Data = ByteString.CopyFrom(data.Span),
};

return this;
}

IFrameBuilder IFrameBuilder.AddEffectiveConfig(IEnumerable<EffectiveConfigFile> files)
{
this.EnsureInitialized();
Expand Down
28 changes: 28 additions & 0 deletions src/OpenTelemetry.OpAmp.Client/Internal/FrameDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,34 @@ AgentToServer BuildEffectiveConfigMessage(FrameBuilder fb)
}
}

public async Task DispatchCustomCapabilitiesAsync(IEnumerable<string> capabilities, CancellationToken token)
{
await this.DispatchFrameAsync(
BuildCustomCapabilitiesMessage,
OpAmpClientEventSource.Log.SendingCustomCapabilitiesMessage,
OpAmpClientEventSource.Log.SendCustomCapabilitiesMessageException,
token).ConfigureAwait(false);

AgentToServer BuildCustomCapabilitiesMessage(FrameBuilder fb)
{
return fb.StartBaseMessage().AddCustomCapabilities(capabilities).Build();
}
}

public async Task DispatchCustomMessageAsync(string capability, string type, ReadOnlyMemory<byte> data, CancellationToken token)
{
await this.DispatchFrameAsync(
BuildCustomMessageMessage,
OpAmpClientEventSource.Log.SendingCustomMessageMessage,
OpAmpClientEventSource.Log.SendCustomMessageMessageException,
token).ConfigureAwait(false);

AgentToServer BuildCustomMessageMessage(FrameBuilder fb)
{
return fb.StartBaseMessage().AddCustomMessage(capability, type, data).Build();
}
}

public void Dispose()
{
this.syncRoot.Dispose();
Expand Down
4 changes: 4 additions & 0 deletions src/OpenTelemetry.OpAmp.Client/Internal/IFrameBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ internal interface IFrameBuilder

IFrameBuilder AddCapabilities();

IFrameBuilder AddCustomCapabilities(IEnumerable<string> capabilities);

IFrameBuilder AddEffectiveConfig(IEnumerable<EffectiveConfigFile> files);

IFrameBuilder AddCustomMessage(string capability, string type, ReadOnlyMemory<byte> data);

AgentToServer Build();
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@ internal class OpAmpClientEventSource : EventSource
private const int EventIdSendingHeartbeatMessage = 1_001;
private const int EventIdSendingAgentDisconnectMessage = 1_002;
private const int EventIdSendingEffectiveConfigMessage = 1_003;
private const int EventIdSendingCustomCapabilitiesMessage = 1_004;
private const int EventIdSendingCustomMessageMessage = 1_005;

// FrameDispatcher error messages 1100-1199
private const int EventIdFailedToSendIdentificationMessage = 1_100;
private const int EventIdFailedToSendHeartbeatMessage = 1_101;
private const int EventIdFailedToSendAgentDisconnectMessage = 1_102;
private const int EventIdFailedToSendEffectiveConfigMessage = 1_103;
private const int EventIdFailedToSendCustomCapabilitiesMessage = 1_104;
private const int EventIdFailedToSendCustomMessageMessage = 1_105;

[Event(EventIdInvalidWsFrame, Message = "Received invalid WebSocket frame header: {0}. Dropping the frame.", Level = EventLevel.Warning)]
public void InvalidWsFrame(string errorMessage)
Expand Down Expand Up @@ -113,6 +117,18 @@ public void SendingEffectiveConfigMessage()
this.WriteEvent(EventIdSendingEffectiveConfigMessage);
}

[Event(EventIdSendingCustomCapabilitiesMessage, Message = "Sending custom capabilities message.", Level = EventLevel.Informational)]
public void SendingCustomCapabilitiesMessage()
{
this.WriteEvent(EventIdSendingCustomCapabilitiesMessage);
}

[Event(EventIdSendingCustomMessageMessage, Message = "Sending custom message.", Level = EventLevel.Informational)]
public void SendingCustomMessageMessage()
{
this.WriteEvent(EventIdSendingCustomMessageMessage);
}

[NonEvent]
public void SendIdentificationMessageException(Exception ex)
{
Expand Down Expand Up @@ -172,4 +188,34 @@ public void FailedToSendEffectiveConfigMessage(string exception)
{
this.WriteEvent(EventIdFailedToSendEffectiveConfigMessage, exception);
}

[NonEvent]
public void SendCustomCapabilitiesMessageException(Exception ex)
{
if (!this.IsEnabled(EventLevel.Error, EventKeywords.All))
{
this.FailedToSendCustomCapabilitiesMessage(ex.ToInvariantString());
}
}

[Event(EventIdFailedToSendCustomCapabilitiesMessage, Message = "Failed to send custom capabilities message: {0}", Level = EventLevel.Error)]
public void FailedToSendCustomCapabilitiesMessage(string exception)
{
this.WriteEvent(EventIdFailedToSendCustomCapabilitiesMessage, exception);
}

[NonEvent]
public void SendCustomMessageMessageException(Exception ex)
{
if (!this.IsEnabled(EventLevel.Error, EventKeywords.All))
{
this.FailedToSendCustomMessageMessage(ex.ToInvariantString());
}
}

[Event(EventIdFailedToSendCustomMessageMessage, Message = "Failed to send a custom message: {0}", Level = EventLevel.Error)]
public void FailedToSendCustomMessageMessage(string exception)
{
this.WriteEvent(EventIdFailedToSendCustomMessageMessage, exception);
}
}
24 changes: 24 additions & 0 deletions src/OpenTelemetry.OpAmp.Client/OpAmpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,30 @@ public Task SendEffectiveConfigAsync(IEnumerable<EffectiveConfigFile> files, Can
return this.dispatcher.DispatchEffectiveConfigAsync(files, cancellationToken);
}

/// <summary>
/// Reports custom capabilities supported by the agent.
/// </summary>
/// <param name="capabilities">Capabilities list.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous send operation.</returns>
public Task SendCustomCapabilitiesAsync(IEnumerable<string> capabilities, CancellationToken cancellationToken = default)
{
return this.dispatcher.DispatchCustomCapabilitiesAsync(capabilities, cancellationToken);
}

/// <summary>
/// Sends a custom message related to a supported custom capability.
/// </summary>
/// <param name="capability">Capability that matches a reported custom capability.</param>
/// <param name="type">Type of message within the capability.</param>
/// <param name="data">Contents of the message.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous send operation.</returns>
public Task SendCustomMessageAsync(string capability, string type, ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)
{
return this.dispatcher.DispatchCustomMessageAsync(capability, type, data, cancellationToken);
}

/// <summary>
/// Disposes the OpAmpClient instance and releases all associated resources.
/// </summary>
Expand Down
63 changes: 63 additions & 0 deletions test/OpenTelemetry.OpAmp.Client.Tests/OpAmpClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,69 @@ internal async Task SendsEffectiveConfigFile_FromFile()
Assert.Equal(fileContentType, actualConfig.ContentType);
}

[Fact]
internal async Task SendsCustomCapabilities()
{
// Setup OpAMP server
using var opAmpServer = new OpAmpFakeHttpServer(false);
var opAmpEndpoint = opAmpServer.Endpoint;

using var client = new OpAmpClient(o =>
{
o.ServerUrl = opAmpEndpoint;
});

// Setup content
string[] capabilities = ["custom-c1", "custom-c2", "custom-c3"];

// Act
await client.StartAsync();
await client.SendCustomCapabilitiesAsync(capabilities);
await client.StopAsync();

// Assert received frames
var frames = opAmpServer.GetFrames();
var actualCapabilities = frames[1].CustomCapabilities.Capabilities;

Assert.Equal(3, frames.Count); // 3 frames: 1 identification, 2 custom capabilities, 3 disconnect
Assert.Equal(capabilities, actualCapabilities);
}

[Fact]
internal async Task SendsCustomMessage()
{
// Setup OpAMP server
using var opAmpServer = new OpAmpFakeHttpServer(false);
var opAmpEndpoint = opAmpServer.Endpoint;

using var client = new OpAmpClient(o =>
{
o.ServerUrl = opAmpEndpoint;
});

// Setup content
const string capability = "custom-c1";
const string type = "custom-type-1";
const string messageContent = "a custom message contents";
var message = Encoding.UTF8.GetBytes(messageContent);

// Act
await client.StartAsync();
await client.SendCustomMessageAsync(capability, type, message);
await client.StopAsync();

// Assert received frames
var frames = opAmpServer.GetFrames();
var actualCapability = frames[1].CustomMessage.Capability;
var actualType = frames[1].CustomMessage.Type;
var actualMessageContent = frames[1].CustomMessage.Data.ToStringUtf8();

Assert.Equal(3, frames.Count); // 3 frames: 1 identification, 2 custom message, 3 disconnect
Assert.Equal(capability, actualCapability);
Assert.Equal(type, actualType);
Assert.Equal(messageContent, actualMessageContent);
}

internal class CapabilityTestData
: TheoryData<Action<OpAmpClientSettings>, IEnumerable<AgentCapabilities>, IEnumerable<AgentCapabilities>>
{
Expand Down