Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
@@ -1,7 +1,19 @@
OpenTelemetry.OpAmp.Client.Listeners.IOpAmpListener<TMessage>
OpenTelemetry.OpAmp.Client.Listeners.IOpAmpListener<TMessage>.HandleMessage(TMessage! message) -> void
OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile
OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.BodyLength.get -> int
OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.ContentType.get -> string?
OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.GetBodyBytes() -> byte[]!
OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.Name.get -> string!
OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.TryGetBody(System.Span<byte> destination, out int bytesWritten) -> bool
OpenTelemetry.OpAmp.Client.Messages.OpAmpMessage
OpenTelemetry.OpAmp.Client.Messages.OpAmpMessage.OpAmpMessage() -> void
OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage
OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.AgentConfigMap.get -> System.Collections.Generic.IReadOnlyDictionary<string!, OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile!>!
OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.GetConfigHashBytes() -> byte[]!
OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.GetConfigHashUtf8String() -> string!
OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.HashLength.get -> int
OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.TryGetConfigHash(System.Span<byte> destination, out int bytesWritten) -> bool
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
Expand Down
5 changes: 5 additions & 0 deletions src/OpenTelemetry.OpAmp.Client/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
* Add setting to configure the factory used to create `HttpClient` instances
used for the OpAMP Plain HTTP transport.
([#3589](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3589))

* Add support for subscribing and unsubscribing to messages from the OpAMP server.
([#3593](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3593))

* Clean up directories and namespaces for public API.
([#3612](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3612))

* Expose public `RemoteConfigMessage`.
([#3614](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3614))

## 0.1.0-alpha.3

Released 2025-Nov-13
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using Google.Protobuf;

namespace OpenTelemetry.OpAmp.Client.Messages;

/// <summary>
/// Represents an agent configuration file.
/// </summary>
public class AgentConfigFile
{
private readonly ByteString body;

internal AgentConfigFile(string name, global::OpAmp.Proto.V1.AgentConfigFile agentConfigFile)
{
this.body = agentConfigFile.Body ?? ByteString.Empty;
this.ContentType = agentConfigFile.ContentType;
this.Name = name;
}

/// <summary>
/// Gets the length, in bytes, of the configuration file body.
/// </summary>
public int BodyLength => this.body.Length;

/// <summary>
/// Gets the MIME Content-Type that describes the data contained in the body of the remote configuration file.
/// </summary>
public string? ContentType { get; }

/// <summary>
/// Gets the name of this configuration file.
/// </summary>
public string Name { get; }

/// <summary>
/// Returns the configuration file body as a byte array.
/// </summary>
/// <returns>A byte array containing the contents of the message body. The array is empty if the body has no content.</returns>
public byte[] GetBodyBytes() => this.body.ToByteArray() ?? [];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if byte[]? would be better to be able to distinguish between "not set" and "genuinely empty" (assuming such a distinction is meaningful here).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear to me from the spec if this could ever be null. In theory, not as it should only send the config if there's something to send. My gut is that the expected scenario is there is always some data so avoiding making this nullable and requiring the user to null check is slightly more pleasant. The spec specifically calls out that the key may be empty, but doesn't specify scenarios were the config could be null.

@Kielek Kielek Dec 15, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One topic to the public contract, I have missed in previous PR.

Shouldn't ReadOnlySpan<byte> be a better option here? the body, exposes Span property. We could avoid allocations.

It is related to other places in the public API

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kielek We did have that exposed at one point but I think at the time I'd missed that ByteString has a span directly accessible. I'll update as we can get rid of the methods and TryGet pattern, just relying on exposing the Span for consumers to do whatever they need with.


/// <summary>
/// Attempts to copy the configuration file body to the specified destination buffer.
/// </summary>
/// <remarks>If the body is empty, no data is written and the method returns <c>true</c> with <paramref name="bytesWritten"/> set to
/// 0. If the destination buffer is too small to hold the body content, no data is written, <paramref name="bytesWritten"/> is set to
/// 0, and the method returns <c>false</c>.</remarks>
/// <param name="destination">The buffer that receives the body bytes. Must be large enough to hold the entire body content.</param>
/// <param name="bytesWritten">When this method returns, contains the number of bytes successfully written to the destination buffer.</param>
/// <returns><c>true</c> if the body was successfully copied to the destination buffer or if the body is empty; otherwise, <c>false</c>.</returns>
public bool TryGetBody(Span<byte> destination, out int bytesWritten)
{
if (this.body.IsEmpty)
{
bytesWritten = 0;
return true;
}

if (destination.Length < this.body.Length)
{
bytesWritten = 0;
return false;
}

this.body.Span.CopyTo(destination);
bytesWritten = this.body.Length;
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using Google.Protobuf;
using OpAmp.Proto.V1;

namespace OpenTelemetry.OpAmp.Client.Messages;

/// <summary>
/// Represents an OpAMP server-to-agent remote configuration message.
/// </summary>
public class RemoteConfigMessage : OpAmpMessage
{
private readonly Dictionary<string, AgentConfigFile> agentConfigMap;
private readonly ByteString configHash;

internal RemoteConfigMessage(AgentRemoteConfig agentRemoteConfig)
{
this.agentConfigMap = new Dictionary<string, AgentConfigFile>(agentRemoteConfig.Config.ConfigMap.Count, StringComparer.Ordinal);

foreach (var config in agentRemoteConfig.Config.ConfigMap)
{
if (config.Value is not null)
{
this.agentConfigMap[config.Key] = new AgentConfigFile(config.Key, config.Value);
}
}

this.configHash = agentRemoteConfig.ConfigHash;
}

/// <summary>
/// Gets a dictionary of agent configuration files, keyed by the name of the configuration file.
/// </summary>
public IReadOnlyDictionary<string, AgentConfigFile> AgentConfigMap => this.agentConfigMap;

/// <summary>
/// Gets the length, in bytes, of the configuration hash.
/// </summary>
public int HashLength => this.configHash.Length;

/// <summary>
/// Returns the configuration hash as a byte array.
/// </summary>
/// <returns>A <see cref="byte"/> array containing the hash of the remote configuration.</returns>
public byte[] GetConfigHashBytes() => this.configHash.ToByteArray();

/// <summary>
/// Returns the configuration hash as a UTF-8 string.
/// </summary>
/// <returns>A <see cref="string"/> representing the UTF-8 encoded configuration hash.</returns>
public string GetConfigHashUtf8String() => this.configHash.ToStringUtf8();

/// <summary>
/// Attempts to copy the configuration hash to the specified destination buffer.
/// </summary>
/// <remarks>If the hash is empty, no data is written and the method returns <c>true</c> with <paramref name="bytesWritten"/> set to
/// 0. If the destination buffer is too small to hold the hash, no data is written, <paramref name="bytesWritten"/> is set to
/// 0, and the method returns <c>false</c>.</remarks>
/// <param name="destination">The buffer that receives the hash bytes. Must be large enough to hold the entire hash content.</param>
/// <param name="bytesWritten">When this method returns, contains the number of bytes successfully written to the destination buffer.</param>
/// <returns><c>true</c> if the hash was successfully copied to the destination buffer or if the hash is empty; otherwise, <c>false</c>.</returns>
public bool TryGetConfigHash(Span<byte> destination, out int bytesWritten)
{
if (this.configHash is null)
{
bytesWritten = 0;
return false;
}

if (this.configHash.IsEmpty)
{
bytesWritten = 0;
return true;
}

if (destination.Length < this.configHash.Length)
{
bytesWritten = 0;
return false;
}

this.configHash.Span.CopyTo(destination);
bytesWritten = this.configHash.Length;
return true;
}
}
Loading