diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91cf639409..5d9ba2c1ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -594,7 +594,6 @@ jobs: || contains(needs.detect-changes.outputs.changes, 'instrumentation-servicefabricremoting') || contains(needs.detect-changes.outputs.changes, 'instrumentation-sqlclient') || contains(needs.detect-changes.outputs.changes, 'instrumentation-stackexchangeredis') - || contains(needs.detect-changes.outputs.changes, 'opamp-client') || contains(needs.detect-changes.outputs.changes, 'resources-aws') || contains(needs.detect-changes.outputs.changes, 'resources-azure') || contains(needs.detect-changes.outputs.changes, 'resources-container') diff --git a/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md b/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md index d46e77a6fd..752d66e5f2 100644 --- a/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md +++ b/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased -* Initial release of - `OpenTelemetry.OpAmp.Client` - project. +* Initial release of `OpenTelemetry.OpAmp.Client` project. + ([#2917](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2917)) +* Added support for OpAMP Plain HTTP transport. + ([#2926](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2926)) diff --git a/src/OpenTelemetry.OpAmp.Client/FrameBuilder.cs b/src/OpenTelemetry.OpAmp.Client/FrameBuilder.cs new file mode 100644 index 0000000000..84e53d6dd8 --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/FrameBuilder.cs @@ -0,0 +1,57 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Google.Protobuf; +using OpAmp.Proto.V1; + +namespace OpenTelemetry.OpAmp.Client; + +internal sealed class FrameBuilder : IFrameBuilder +{ + private readonly OpAmpClientSettings settings; + + private AgentToServer? currentMessage; + private ByteString instanceUid; + private ulong sequenceNum; + + public FrameBuilder(OpAmpClientSettings settings) + { + this.settings = settings; + this.instanceUid = ByteString.CopyFrom(this.settings.InstanceUid.ToByteArray()); + } + + public IFrameBuilder StartBaseMessage() + { + if (this.currentMessage != null) + { + throw new InvalidOperationException("Message base is already initialized."); + } + + var message = new AgentToServer() + { + InstanceUid = this.instanceUid, + SequenceNum = ++this.sequenceNum, + }; + + this.currentMessage = message; + return this; + } + + AgentToServer IFrameBuilder.Build() + { + if (this.currentMessage == null) + { + throw new InvalidOperationException("Message base is not initialized."); + } + + var message = this.currentMessage; + this.currentMessage = null; // Reset for the next message + + return message; + } + + public void Reset() + { + this.currentMessage = null; + } +} diff --git a/src/OpenTelemetry.OpAmp.Client/FrameDispatcher.cs b/src/OpenTelemetry.OpAmp.Client/FrameDispatcher.cs new file mode 100644 index 0000000000..8b0d53e14c --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/FrameDispatcher.cs @@ -0,0 +1,60 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Internal; +using OpenTelemetry.OpAmp.Client.Transport; + +namespace OpenTelemetry.OpAmp.Client; + +internal sealed class FrameDispatcher : IDisposable +{ + private readonly IOpAmpTransport transport; + private readonly FrameBuilder frameBuilder; + private readonly SemaphoreSlim syncRoot = new(1, 1); + + public FrameDispatcher(IOpAmpTransport transport, OpAmpClientSettings settings) + { + Guard.ThrowIfNull(transport, nameof(transport)); + Guard.ThrowIfNull(settings, nameof(settings)); + + this.transport = transport; + this.frameBuilder = new FrameBuilder(settings); + } + + // TODO: May need to redesign to request only partials + // so any other message waiting to be sent can be included to optimize transport usage and locking time. + public async Task DispatchServerFrameAsync(CancellationToken token) + { + await this.syncRoot.WaitAsync(token) + .ConfigureAwait(false); + + try + { + var message = this.frameBuilder + .StartBaseMessage() + .Build(); + + // TODO: change to proper logging + Console.WriteLine("Sending identification message."); + + await this.transport.SendAsync(message, token) + .ConfigureAwait(false); + } + catch (Exception ex) + { + // TODO: change to proper logging + Console.WriteLine($"[Error]: {ex.Message}"); + + this.frameBuilder.Reset(); // Reset the builder in case of failure + } + finally + { + this.syncRoot.Release(); + } + } + + public void Dispose() + { + this.syncRoot.Dispose(); + } +} diff --git a/src/OpenTelemetry.OpAmp.Client/FrameProcessor.cs b/src/OpenTelemetry.OpAmp.Client/FrameProcessor.cs new file mode 100644 index 0000000000..cab4aea259 --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/FrameProcessor.cs @@ -0,0 +1,123 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using OpAmp.Proto.V1; +using OpenTelemetry.Internal; +using OpenTelemetry.OpAmp.Client.Listeners; +using OpenTelemetry.OpAmp.Client.Listeners.Messages; + +namespace OpenTelemetry.OpAmp.Client; + +internal sealed class FrameProcessor +{ + private readonly ConcurrentDictionary> listeners = []; + + public void Subscribe(IOpAmpListener listener) + where T : IOpAmpMessage + { + Guard.ThrowIfNull(listener, nameof(listener)); + + // It is expected to be much more read-heavy than write-heavy, so we use ImmutableList for thread safety + this.listeners.AddOrUpdate( + typeof(T), + _ => [listener], + (_, list) => list.Add(listener)); + } + + public void Unsubscribe(IOpAmpListener listener) + where T : IOpAmpMessage + { + Guard.ThrowIfNull(listener, nameof(listener)); + + this.listeners.AddOrUpdate( + typeof(T), + _ => ImmutableList.Empty, + (_, list) => + { + if (list.Count == 1 && list[0] == listener) + { + return ImmutableList.Empty; + } + + return list.Remove(listener); + }); + } + + public void OnServerFrame(ReadOnlySequence sequence) + { + this.Deserialize(sequence); + } + + private void Deserialize(ReadOnlySequence sequence) + { + var message = ServerToAgent.Parser.ParseFrom(sequence); + + if (message.ErrorResponse != null) + { + this.Dispatch(new ErrorResponseMessage(message.ErrorResponse)); + } + + if (message.RemoteConfig != null) + { + this.Dispatch(new RemoteConfigMessage(message.RemoteConfig)); + } + + if (message.ConnectionSettings != null) + { + this.Dispatch(new ConnectionSettingsMessage(message.ConnectionSettings)); + } + + if (message.PackagesAvailable != null) + { + this.Dispatch(new PackagesAvailableMessage(message.PackagesAvailable)); + } + + if (message.Flags != 0) + { + this.Dispatch(new FlagsMessage((ServerToAgentFlags)message.Flags)); + } + + if (message.Capabilities != 0) + { + this.Dispatch(new CapabilitiesMessage((ServerCapabilities)message.Capabilities)); + } + + if (message.AgentIdentification != null) + { + this.Dispatch(new AgentIdentificationMessage(message.AgentIdentification)); + } + + if (message.Command != null) + { + this.Dispatch(new CommandMessage(message.Command)); + } + + if (message.CustomCapabilities != null) + { + this.Dispatch(new CustomCapabilitiesMessage(message.CustomCapabilities)); + } + + if (message.CustomMessage != null) + { + this.Dispatch(new CustomMessageMessage(message.CustomMessage)); + } + } + + private void Dispatch(T message) + where T : IOpAmpMessage + { + if (this.listeners.TryGetValue(typeof(T), out var list)) + { + foreach (var listener in list) + { + if (listener is IOpAmpListener typedListener) + { + typedListener.HandleMessage(message); + } + } + } + } +} diff --git a/src/OpenTelemetry.OpAmp.Client/IFrameBuilder.cs b/src/OpenTelemetry.OpAmp.Client/IFrameBuilder.cs new file mode 100644 index 0000000000..52afa3ad46 --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/IFrameBuilder.cs @@ -0,0 +1,11 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpAmp.Proto.V1; + +namespace OpenTelemetry.OpAmp.Client; + +internal interface IFrameBuilder +{ + AgentToServer Build(); +} diff --git a/src/OpenTelemetry.OpAmp.Client/Listeners/IOpAmpListener.cs b/src/OpenTelemetry.OpAmp.Client/Listeners/IOpAmpListener.cs new file mode 100644 index 0000000000..fed4857322 --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Listeners/IOpAmpListener.cs @@ -0,0 +1,14 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.OpAmp.Client.Listeners; + +internal interface IOpAmpListener +{ +} + +internal interface IOpAmpListener : IOpAmpListener + where TMessage : IOpAmpMessage +{ + void HandleMessage(TMessage message); +} diff --git a/src/OpenTelemetry.OpAmp.Client/Listeners/IOpAmpMessage.cs b/src/OpenTelemetry.OpAmp.Client/Listeners/IOpAmpMessage.cs new file mode 100644 index 0000000000..ef78a2fe91 --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Listeners/IOpAmpMessage.cs @@ -0,0 +1,8 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.OpAmp.Client.Listeners; + +internal interface IOpAmpMessage +{ +} diff --git a/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/AgentIdentificationMessage.cs b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/AgentIdentificationMessage.cs new file mode 100644 index 0000000000..64587a5c78 --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/AgentIdentificationMessage.cs @@ -0,0 +1,16 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpAmp.Proto.V1; + +namespace OpenTelemetry.OpAmp.Client.Listeners.Messages; + +internal class AgentIdentificationMessage : IOpAmpMessage +{ + public AgentIdentificationMessage(AgentIdentification agentIdentification) + { + this.AgentIdentification = agentIdentification; + } + + public AgentIdentification AgentIdentification { get; set; } +} diff --git a/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/CapabilitiesMessage.cs b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/CapabilitiesMessage.cs new file mode 100644 index 0000000000..4cc7550b93 --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/CapabilitiesMessage.cs @@ -0,0 +1,16 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpAmp.Proto.V1; + +namespace OpenTelemetry.OpAmp.Client.Listeners.Messages; + +internal class CapabilitiesMessage : IOpAmpMessage +{ + public CapabilitiesMessage(ServerCapabilities capabilities) + { + this.Capabilities = capabilities; + } + + public ServerCapabilities Capabilities { get; set; } +} diff --git a/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/CommandMessage.cs b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/CommandMessage.cs new file mode 100644 index 0000000000..420848fb0b --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/CommandMessage.cs @@ -0,0 +1,16 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpAmp.Proto.V1; + +namespace OpenTelemetry.OpAmp.Client.Listeners.Messages; + +internal class CommandMessage : IOpAmpMessage +{ + public CommandMessage(ServerToAgentCommand command) + { + this.Command = command; + } + + public ServerToAgentCommand Command { get; set; } +} diff --git a/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/ConnectionSettingsMessage.cs b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/ConnectionSettingsMessage.cs new file mode 100644 index 0000000000..71e82ee9d3 --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/ConnectionSettingsMessage.cs @@ -0,0 +1,16 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpAmp.Proto.V1; + +namespace OpenTelemetry.OpAmp.Client.Listeners.Messages; + +internal class ConnectionSettingsMessage : IOpAmpMessage +{ + public ConnectionSettingsMessage(ConnectionSettingsOffers connectionSettingsOffers) + { + this.ConnectionSettings = connectionSettingsOffers; + } + + public ConnectionSettingsOffers ConnectionSettings { get; set; } +} diff --git a/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/CustomCapabilitiesMessage.cs b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/CustomCapabilitiesMessage.cs new file mode 100644 index 0000000000..38d23c499b --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/CustomCapabilitiesMessage.cs @@ -0,0 +1,16 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpAmp.Proto.V1; + +namespace OpenTelemetry.OpAmp.Client.Listeners.Messages; + +internal class CustomCapabilitiesMessage : IOpAmpMessage +{ + public CustomCapabilitiesMessage(CustomCapabilities customCapabilities) + { + this.CustomCapabilities = customCapabilities; + } + + public CustomCapabilities CustomCapabilities { get; set; } +} diff --git a/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/CustomMessageMessage.cs b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/CustomMessageMessage.cs new file mode 100644 index 0000000000..3ba23b857e --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/CustomMessageMessage.cs @@ -0,0 +1,16 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpAmp.Proto.V1; + +namespace OpenTelemetry.OpAmp.Client.Listeners.Messages; + +internal class CustomMessageMessage : IOpAmpMessage +{ + public CustomMessageMessage(CustomMessage customMessage) + { + this.CustomMessage = customMessage; + } + + public CustomMessage CustomMessage { get; set; } +} diff --git a/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/ErrorResponseMessage.cs b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/ErrorResponseMessage.cs new file mode 100644 index 0000000000..c22699cdac --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/ErrorResponseMessage.cs @@ -0,0 +1,16 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpAmp.Proto.V1; + +namespace OpenTelemetry.OpAmp.Client.Listeners.Messages; + +internal class ErrorResponseMessage : IOpAmpMessage +{ + public ErrorResponseMessage(ServerErrorResponse serverErrorResponse) + { + this.ErrorResponse = serverErrorResponse; + } + + public ServerErrorResponse ErrorResponse { get; set; } +} diff --git a/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/FlagsMessage.cs b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/FlagsMessage.cs new file mode 100644 index 0000000000..22e76008b7 --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/FlagsMessage.cs @@ -0,0 +1,16 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpAmp.Proto.V1; + +namespace OpenTelemetry.OpAmp.Client.Listeners.Messages; + +internal class FlagsMessage : IOpAmpMessage +{ + public FlagsMessage(ServerToAgentFlags flags) + { + this.Flags = flags; + } + + public ServerToAgentFlags Flags { get; set; } +} diff --git a/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/PackagesAvailableMessage.cs b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/PackagesAvailableMessage.cs new file mode 100644 index 0000000000..6a47433e09 --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/PackagesAvailableMessage.cs @@ -0,0 +1,16 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpAmp.Proto.V1; + +namespace OpenTelemetry.OpAmp.Client.Listeners.Messages; + +internal class PackagesAvailableMessage : IOpAmpMessage +{ + public PackagesAvailableMessage(PackagesAvailable packageAvailable) + { + this.PackagesAvailable = packageAvailable; + } + + public PackagesAvailable PackagesAvailable { get; set; } +} diff --git a/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/RemoteConfigMessage.cs b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/RemoteConfigMessage.cs new file mode 100644 index 0000000000..ebf8acd01c --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Listeners/Messages/RemoteConfigMessage.cs @@ -0,0 +1,16 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpAmp.Proto.V1; + +namespace OpenTelemetry.OpAmp.Client.Listeners.Messages; + +internal class RemoteConfigMessage : IOpAmpMessage +{ + public RemoteConfigMessage(AgentRemoteConfig agentRemoteConfig) + { + this.RemoteConfig = agentRemoteConfig; + } + + public AgentRemoteConfig RemoteConfig { get; set; } +} diff --git a/src/OpenTelemetry.OpAmp.Client/OpAmpClient.cs b/src/OpenTelemetry.OpAmp.Client/OpAmpClient.cs new file mode 100644 index 0000000000..fb55f7e8f8 --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/OpAmpClient.cs @@ -0,0 +1,33 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.OpAmp.Client.Transport; +using OpenTelemetry.OpAmp.Client.Transport.Http; + +namespace OpenTelemetry.OpAmp.Client; + +internal sealed class OpAmpClient +{ + private readonly OpAmpClientSettings settings = new(); + private readonly FrameProcessor processor = new(); + private readonly IOpAmpTransport transport; + + public OpAmpClient(Action? configure = null) + { + configure?.Invoke(this.settings); + + this.transport = ConstructTransport(this.settings, this.processor); + } + +#pragma warning disable CA1859 // Use concrete types when possible for improved performance + private static IOpAmpTransport ConstructTransport(OpAmpClientSettings settings, FrameProcessor processor) +#pragma warning restore CA1859 // Use concrete types when possible for improved performance + { + return settings.ConnectionType switch + { + ConnectionType.WebSocket => throw new NotImplementedException("WebSocket transport is not available."), + ConnectionType.Http => new PlainHttpTransport(settings.ServerUrl, processor), + _ => throw new NotSupportedException("Unsupported transport type"), + }; + } +} diff --git a/src/OpenTelemetry.OpAmp.Client/OpAmpClientSettings.cs b/src/OpenTelemetry.OpAmp.Client/OpAmpClientSettings.cs new file mode 100644 index 0000000000..908e101a3b --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/OpAmpClientSettings.cs @@ -0,0 +1,46 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.OpAmp.Client; + +/// +/// Specifies the type of transport protocol to be used for communication. +/// +/// This enumeration defines the available transport protocols for communication. Use for WebSocket-based communication, or for +/// HTTP-based communication. +internal enum ConnectionType +{ + /// + /// Use HTTP transport. + /// + Http = 0, + + /// + /// Use WebSocket transport. + /// + WebSocket = 1, +} + +internal class OpAmpClientSettings +{ + /// + /// Gets or sets the unique identifier for the current instance. + /// + public Guid InstanceUid { get; set; } +#if NET9_0_OR_GREATER + = Guid.CreateVersion7(); +#else + = Guid.NewGuid(); +#endif + + /// + /// Gets or sets the chosen metrics schema to write. + /// + public ConnectionType ConnectionType { get; set; } = ConnectionType.Http; + + /// + /// Gets or sets the server URL to connect to. + /// + public Uri ServerUrl { get; set; } = new("https://localhost:4320/v1/opamp"); +} diff --git a/src/OpenTelemetry.OpAmp.Client/OpenTelemetry.OpAmp.Client.csproj b/src/OpenTelemetry.OpAmp.Client/OpenTelemetry.OpAmp.Client.csproj index 0b64177cfb..0defcc2a92 100644 --- a/src/OpenTelemetry.OpAmp.Client/OpenTelemetry.OpAmp.Client.csproj +++ b/src/OpenTelemetry.OpAmp.Client/OpenTelemetry.OpAmp.Client.csproj @@ -1,7 +1,7 @@  - $(NetMinimumSupportedVersion);$(NetStandardMinimumSupportedVersion);$(NetFrameworkMinimumSupportedVersion) + net9.0;$(NetMinimumSupportedVersion);$(NetStandardMinimumSupportedVersion);$(NetFrameworkMinimumSupportedVersion) OpAMP Client for OpenTelemetry .NET. $(PackageTags);distributed-tracing;opamp;opamp-client OpAmp.Client- @@ -13,4 +13,32 @@ true + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OpenTelemetry.OpAmp.Client/Protos/anyvalue.proto b/src/OpenTelemetry.OpAmp.Client/Protos/anyvalue.proto new file mode 100644 index 0000000000..9a3acd1131 --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Protos/anyvalue.proto @@ -0,0 +1,68 @@ +// Copyright 2019, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file is copied and modified from https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/common/v1/common.proto +// Modifications: +// - Removal of unneeded InstrumentationLibrary and StringKeyValue messages. +// - Change of go_package to reference a package in this repo. +// - Removal of gogoproto usage. + +syntax = "proto3"; + +package opamp.proto.v1; + +option go_package = "github.com/open-telemetry/opamp-go/protobufs"; +option csharp_namespace = "OpAmp.Proto.V1"; + +// AnyValue is used to represent any type of attribute value. AnyValue may contain a +// primitive value such as a string or integer or it may contain an arbitrary nested +// object containing arrays, key-value lists and primitives. +message AnyValue { + // The value is one of the listed fields. It is valid for all values to be unspecified + // in which case this AnyValue is considered to be "null". + oneof value { + string string_value = 1; + bool bool_value = 2; + int64 int_value = 3; + double double_value = 4; + ArrayValue array_value = 5; + KeyValueList kvlist_value = 6; + bytes bytes_value = 7; + } +} + +// ArrayValue is a list of AnyValue messages. We need ArrayValue as a message +// since oneof in AnyValue does not allow repeated fields. +message ArrayValue { + // Array of values. The array may be empty (contain 0 elements). + repeated AnyValue values = 1; +} + +// KeyValueList is a list of KeyValue messages. We need KeyValueList as a message +// since `oneof` in AnyValue does not allow repeated fields. Everywhere else where we need +// a list of KeyValue messages (e.g. in Span) we use `repeated KeyValue` directly to +// avoid unnecessary extra wrapping (which slows down the protocol). The 2 approaches +// are semantically equivalent. +message KeyValueList { + // A collection of key/value pairs of key-value pairs. The list may be empty (may + // contain 0 elements). + repeated KeyValue values = 1; +} + +// KeyValue is a key-value pair that is used to store Span attributes, Link +// attributes, etc. +message KeyValue { + string key = 1; + AnyValue value = 2; +} diff --git a/src/OpenTelemetry.OpAmp.Client/Protos/opamp.proto b/src/OpenTelemetry.OpAmp.Client/Protos/opamp.proto new file mode 100644 index 0000000000..d46a379510 --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Protos/opamp.proto @@ -0,0 +1,1061 @@ +// Copyright 2021, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// OpAMP: Open Agent Management Protocol (https://github.com/open-telemetry/opamp-spec) + +syntax = "proto3"; + +package opamp.proto.v1; + +import "anyvalue.proto"; + +option go_package = "github.com/open-telemetry/opamp-go/protobufs"; +option csharp_namespace = "OpAmp.Proto.V1"; + +message AgentToServer +{ + // Globally unique identifier of the running instance of the Agent. SHOULD remain + // unchanged for the lifetime of the Agent process. + // MUST be 16 bytes long and SHOULD be generated using the UUID v7 spec. + bytes instance_uid = 1; + + // The sequence number is incremented by 1 for every AgentToServer sent + // by the Agent. This allows the Server to detect that it missed a message when + // it notices that the sequence_num is not exactly by 1 greater than the previously + // received one. + uint64 sequence_num = 2; + + // Data that describes the Agent, its type, where it runs, etc. + // May be omitted if nothing changed since last AgentToServer message. + AgentDescription agent_description = 3; + + // Bitmask of flags defined by AgentCapabilities enum. + // All bits that are not defined in AgentCapabilities enum MUST be set to 0 by + // the Agent. This allows extending the protocol and the AgentCapabilities enum + // in the future such that old Agents automatically report that they don't + // support the new capability. + // This field MUST be always set. + uint64 capabilities = 4; + + // The current health of the Agent and sub-components. The top-level ComponentHealth represents + // the health of the Agent overall. May be omitted if nothing changed since last AgentToServer + // message. + // Status: [Beta] + ComponentHealth health = 5; + + // The current effective configuration of the Agent. The effective configuration is + // the one that is currently used by the Agent. The effective configuration may be + // different from the remote configuration received from the Server earlier, e.g. + // because the Agent uses a local configuration instead (or in addition). + // + // This field SHOULD be unset if the effective config is unchanged since the last + // AgentToServer message. + EffectiveConfig effective_config = 6; + + // The status of the remote config that was previously received from the Server. + // This field SHOULD be unset if the remote config status is unchanged since the + // last AgentToServer message. + RemoteConfigStatus remote_config_status = 7; + + // The list of the Agent packages, including package statuses. This field SHOULD be + // unset if this information is unchanged since the last AgentToServer message for + // this Agent was sent in the stream. + // Status: [Beta] + PackageStatuses package_statuses = 8; + + // AgentDisconnect MUST be set in the last AgentToServer message sent from the + // Agent to the Server. + AgentDisconnect agent_disconnect = 9; + + // Bit flags as defined by AgentToServerFlags bit masks. + uint64 flags = 10; + + // A request to create connection settings. This field is set for flows where + // the Agent initiates the creation of connection settings. + // Status: [Development] + ConnectionSettingsRequest connection_settings_request = 11; + + // A message indicating custom capabilities supported by the Agent. + // Status: [Development] + CustomCapabilities custom_capabilities = 12; + + // A custom message sent from an Agent to the Server. + // Status: [Development] + CustomMessage custom_message = 13; + + // A message indicating the components that are available for configuration on the agent. + // Status: [Development] + AvailableComponents available_components = 14; +} + +enum AgentToServerFlags +{ + AgentToServerFlags_Unspecified = 0; + + // AgentToServerFlags is a bit mask. Values below define individual bits. + + // The Agent requests Server go generate a new instance_uid, which will + // be sent back in ServerToAgent message + AgentToServerFlags_RequestInstanceUid = 0x00000001; +} + +// AgentDisconnect is the last message sent from the Agent to the Server. The Server +// SHOULD forget the association of the Agent instance with the message stream. +// +// If the message stream is closed in the transport layer then the Server SHOULD +// forget association of all Agent instances that were previously established for +// this message stream using AgentConnect message, even if the corresponding +// AgentDisconnect message were not explicitly received from the Agent. +message AgentDisconnect +{ +} + +// ConnectionSettingsRequest is a request from the Agent to the Server to create +// and respond with an offer of connection settings for the Agent. +// Status: [Development] +message ConnectionSettingsRequest +{ + // Request for OpAMP connection settings. If this field is unset + // then the ConnectionSettingsRequest message is empty and is not actionable + // for the Server. + OpAMPConnectionSettingsRequest opamp = 1; + + // In the future we can add request fields for non-OpAMP connection types + // (own telemetry, other connections). +} + +// OpAMPConnectionSettingsRequest is a request for the Server to produce +// a OpAMPConnectionSettings in its response. +// Status: [Development] +message OpAMPConnectionSettingsRequest +{ + // A request to create a client certificate. This is used to initiate a + // Client Signing Request (CSR) flow. + // Required. + CertificateRequest certificate_request = 1; +} + +// Status: [Development] +message CertificateRequest +{ + // PEM-encoded Client Certificate Signing Request (CSR), signed by client's private key. + // The Server SHOULD validate the request and SHOULD respond with a + // OpAMPConnectionSettings where the certificate.cert contains the issued + // certificate. + bytes csr = 1; +} + +// AvailableComponents contains metadata relating to the components included +// within the agent. +// status: [Development] +message AvailableComponents +{ + // A map of a unique component ID to details about the component. + // This may be omitted from the message if the server has not + // explicitly requested it be sent by setting the ReportAvailableComponents + // flag in the previous ServerToAgent message. + map components = 1; + + // Agent-calculated hash of the components. + // This hash should be included in every AvailableComponents message. + bytes hash = 2; +} + +message ComponentDetails +{ + // Extra key/value pairs that may be used to describe the component. + // The key/value pairs are according to semantic conventions, see: + // https://opentelemetry.io/docs/specs/semconv/ + // + // For example, you may use the "code" semantic conventions to + // report the location of the code for a specific component: + // https://opentelemetry.io/docs/specs/semconv/attributes-registry/code/ + // + // Or you may use the "vcs" semantic conventions to report the + // repository the component may be a part of: + // https://opentelemetry.io/docs/specs/semconv/attributes-registry/vcs/ + repeated KeyValue metadata = 1; + + // A map of component ID to sub components details. It can nest as deeply as needed to + // describe the underlying system. + map sub_component_map = 2; +} + + +message ServerToAgent +{ + // Agent instance uid. MUST match the instance_uid field in AgentToServer message. + // Used for multiplexing messages from/to multiple agents using one message stream. + bytes instance_uid = 1; + + // error_response is set if the Server wants to indicate that something went wrong + // during processing of an AgentToServer message. If error_response is set then + // all other fields below must be unset and vice versa, if any of the fields below is + // set then error_response must be unset. + ServerErrorResponse error_response = 2; + + // remote_config field is set when the Server has a remote config offer for the Agent. + AgentRemoteConfig remote_config = 3; + + // This field is set when the Server wants the Agent to change one or more + // of its client connection settings (destination, headers, certificate, etc). + // Status: [Beta] + ConnectionSettingsOffers connection_settings = 4; + + // This field is set when the Server has packages to offer to the Agent. + // Status: [Beta] + PackagesAvailable packages_available = 5; + + // Bit flags as defined by ServerToAgentFlags bit masks. + uint64 flags = 6; + + // Bitmask of flags defined by ServerCapabilities enum. + // All bits that are not defined in ServerCapabilities enum MUST be set to 0 + // by the Server. This allows extending the protocol and the ServerCapabilities + // enum in the future such that old Servers automatically report that they + // don't support the new capability. + // This field MUST be set in the first ServerToAgent sent by the Server and MAY + // be omitted in subsequent ServerToAgent messages by setting it to + // UnspecifiedServerCapability value. + uint64 capabilities = 7; + + // Properties related to identification of the Agent, which can be overridden + // by the Server if needed. + AgentIdentification agent_identification = 8; + + // Allows the Server to instruct the Agent to perform a command, e.g. RESTART. This field should not be specified + // with fields other than instance_uid and capabilities. If specified, other fields will be ignored and the command + // will be performed. + // Status: [Beta] + ServerToAgentCommand command = 9; + + // A message indicating custom capabilities supported by the Server. + // Status: [Development] + CustomCapabilities custom_capabilities = 10; + + // A custom message sent from the Server to an Agent. + // Status: [Development] + CustomMessage custom_message = 11; +} + +enum ServerToAgentFlags +{ + ServerToAgentFlags_Unspecified = 0; + + // Flags is a bit mask. Values below define individual bits. + + // ReportFullState flag can be used by the Server if the Agent did not include the + // particular bit of information in the last status report (which is an allowed + // optimization) but the Server detects that it does not have it (e.g. was + // restarted and lost state). The detection happens using + // AgentToServer.sequence_num values. + // The Server asks the Agent to report full status. + ServerToAgentFlags_ReportFullState = 0x00000001; + + // ReportAvailableComponents flag can be used by the server if the Agent did + // not include the full AvailableComponents message, but only the hash. + // If this flag is specified, the agent will populate available_components.components + // with a full description of the agent's components. + // Status: [Development] + ServerToAgentFlags_ReportAvailableComponents = 0x00000002; +} + +enum ServerCapabilities +{ + // The capabilities field is unspecified. + ServerCapabilities_Unspecified = 0; + + // The Server can accept status reports. This bit MUST be set, since all Server + // MUST be able to accept status reports. + ServerCapabilities_AcceptsStatus = 0x00000001; + // The Server can offer remote configuration to the Agent. + ServerCapabilities_OffersRemoteConfig = 0x00000002; + // The Server can accept EffectiveConfig in AgentToServer. + ServerCapabilities_AcceptsEffectiveConfig = 0x00000004; + // The Server can offer Packages. + // Status: [Beta] + ServerCapabilities_OffersPackages = 0x00000008; + // The Server can accept Packages status. + // Status: [Beta] + ServerCapabilities_AcceptsPackagesStatus = 0x00000010; + // The Server can offer connection settings. + // Status: [Beta] + ServerCapabilities_OffersConnectionSettings = 0x00000020; + // The Server can accept ConnectionSettingsRequest and respond with an offer. + // Status: [Development] + ServerCapabilities_AcceptsConnectionSettingsRequest = 0x00000040; + + // Add new capabilities here, continuing with the least significant unused bit. +} + +// The OpAMPConnectionSettings message is a collection of fields which comprise an +// offer from the Server to the Agent to use the specified settings for OpAMP +// connection. +// Status: [Beta] +message OpAMPConnectionSettings +{ + // OpAMP Server URL This MUST be a WebSocket or HTTP URL and MUST be non-empty, for + // example: "wss://example.com:4318/v1/opamp" + string destination_endpoint = 1; + + // Optional headers to use when connecting. Typically used to set access tokens or + // other authorization headers. For HTTP-based protocols the Agent should + // set these in the request headers. + // For example: + // key="Authorization", Value="Basic YWxhZGRpbjpvcGVuc2VzYW1l". + Headers headers = 2; + + // The Agent should use the offered certificate to connect to the destination + // from now on. If the Agent is able to validate and connect using the offered + // certificate the Agent SHOULD forget any previous client certificates + // for this connection. + // This field is optional: if omitted the client SHOULD NOT use a client-side certificate. + // This field can be used to perform a client certificate revocation/rotation. + TLSCertificate certificate = 3; + + // The Agent MUST periodically send an AgentToServer message if the + // AgentCapabilities_ReportsHeartbeat capability is true. At a minimum the instance_uid + // field MUST be set. + // + // An HTTP Client MUST use the value as polling interval, if heartbeat_interval_seconds is non-zero. + // + // A heartbeat is used to keep the connection active and inform the server that the Agent + // is still alive and active. + // + // If this field has no value or is set to 0, the Agent should not send any heartbeats. + // Status: [Development] + uint64 heartbeat_interval_seconds = 4; + + // Optional connection specific TLS settings. + // Status: [Development] + TLSConnectionSettings tls = 5; +} + +// The TelemetryConnectionSettings message is a collection of fields which comprise an +// offer from the Server to the Agent to use the specified settings for a network +// connection to report own telemetry. +// Status: [Beta] +message TelemetryConnectionSettings +{ + // The value MUST be a full URL an OTLP/HTTP/Protobuf receiver with path. Schema + // SHOULD begin with "https://", for example "https://example.com:4318/v1/metrics" + // The Agent MAY refuse to send the telemetry if the URL begins with "http://". + string destination_endpoint = 1; + + // Optional headers to use when connecting. Typically used to set access tokens or + // other authorization headers. For HTTP-based protocols the Agent should + // set these in the request headers. + // For example: + // key="Authorization", Value="Basic YWxhZGRpbjpvcGVuc2VzYW1l". + Headers headers = 2; + + // The Agent should use the offered certificate to connect to the destination + // from now on. If the Agent is able to validate and connect using the offered + // certificate the Agent SHOULD forget any previous client certificates + // for this connection. + // This field is optional: if omitted the client SHOULD NOT use a client-side certificate. + // This field can be used to perform a client certificate revocation/rotation. + TLSCertificate certificate = 3; + + // Optional connection specific TLS settings. + // Status: [Development] + TLSConnectionSettings tls = 4; +} + +// The OtherConnectionSettings message is a collection of fields which comprise an +// offer from the Server to the Agent to use the specified settings for a network +// connection. It is not required that all fields in this message are specified. +// The Server may specify only some of the fields, in which case it means that +// the Server offers the Agent to change only those fields, while keeping the +// rest of the fields unchanged. +// +// For example the Server may send a ConnectionSettings message with only the +// certificate field set, while all other fields are unset. This means that +// the Server wants the Agent to use a new certificate and continue sending to +// the destination it is currently sending using the current header and other +// settings. +// +// For fields which reference other messages the field is considered unset +// when the reference is unset. +// +// For primitive field (string) we rely on the "flags" to describe that the +// field is not set (this is done to overcome the limitation of old protoc +// compilers don't generate methods that allow to check for the presence of +// the field. +// Status: [Beta] +message OtherConnectionSettings +{ + // A URL, host:port or some other destination specifier. + string destination_endpoint = 1; + + // Optional headers to use when connecting. Typically used to set access tokens or + // other authorization headers. For HTTP-based protocols the Agent should + // set these in the request headers. + // For example: + // key="Authorization", Value="Basic YWxhZGRpbjpvcGVuc2VzYW1l". + Headers headers = 2; + + // The Agent should use the offered certificate to connect to the destination + // from now on. If the Agent is able to validate and connect using the offered + // certificate the Agent SHOULD forget any previous client certificates + // for this connection. + // This field is optional: if omitted the client SHOULD NOT use a client-side certificate. + // This field can be used to perform a client certificate revocation/rotation. + TLSCertificate certificate = 3; + + // Other connection settings. These are Agent-specific and are up to the Agent + // interpret. + map other_settings = 4; + + // Optional connection specific TLS settings. + // Status: [Development] + TLSConnectionSettings tls = 5; +} + + +// TLSConnectionSettings are optional connection settings that can be passed to +// the client in order to specify TLS configuration. +// Status: [Development] +message TLSConnectionSettings +{ + // Provides CA cert contents as a string. + string ca_pem_contents = 1; + + // Load system CA pool alongside any passed CAs. + bool include_system_ca_certs_pool = 2; + + // skip certificate verification. + bool insecure_skip_verify = 3; + + // Miniumum accepted TLS version; default "1.2". + string min_version = 4; + + // Maxiumum accepted TLS version; default "". + string max_version = 5; + + // Explicit list of cipher suites. + repeated string cipher_suites = 6; +} + +// Status: [Beta] +message Headers +{ + repeated Header headers = 1; +} + +// Status: [Beta] +message Header +{ + string key = 1; + string value = 2; +} + +// Status: [Beta] +message TLSCertificate +{ + // The (cert,private_key) pair should be issued and signed by a Certificate + // Authority (CA) that the destination Server recognizes. + // + // It is highly recommended that the private key of the CA certificate is NOT + // stored on the destination Server otherwise compromising the Server will allow + // a malicious actor to issue valid Server certificates which will be automatically + // trusted by all agents and will allow the actor to trivially MITM Agent-to-Server + // traffic of all servers that use this CA certificate for their Server-side + // certificates. + // + // Alternatively the certificate may be self-signed, assuming the Server can + // verify the certificate. + + // PEM-encoded certificate. Required. + bytes cert = 1; + + // PEM-encoded private key of the certificate. Required. + bytes private_key = 2; + + // PEM-encoded certificate of the signing CA. + // Optional. MUST be specified if the certificate is CA-signed. + // Can be stored by TLS-terminating intermediary proxies in order to verify + // the connecting client's certificate in the future. + // It is not recommended that the Agent accepts this CA as an authority for + // any purposes. + bytes ca_cert = 3; +} + +// Status: [Beta] +message ConnectionSettingsOffers +{ + // Hash of all settings, including settings that may be omitted from this message + // because they are unchanged. + bytes hash = 1; + + // Settings to connect to the OpAMP Server. + // If this field is not set then the Agent should assume that the settings are + // unchanged and should continue using existing settings. + // The Agent MUST verify the offered connection settings by actually connecting + // before accepting the setting to ensure it does not loose access to the OpAMP + // Server due to invalid settings. + OpAMPConnectionSettings opamp = 2; + + // Settings to connect to an OTLP metrics backend to send Agent's own metrics to. + // If this field is not set then the Agent should assume that the settings + // are unchanged. + // + // Once accepted the Agent should periodically send to the specified destination + // its own metrics, i.e. metrics of the Agent process and any custom metrics that + // describe the Agent state. + // + // All attributes specified in the identifying_attributes field in AgentDescription + // message SHOULD be also specified in the Resource of the reported OTLP metrics. + // + // Attributes specified in the non_identifying_attributes field in + // AgentDescription message may be also specified in the Resource of the reported + // OTLP metrics, in which case they SHOULD have exactly the same values. + // + // Process metrics MUST follow the conventions for processes: + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/process-metrics.md + TelemetryConnectionSettings own_metrics = 3; + + // Similar to own_metrics, but for traces. + TelemetryConnectionSettings own_traces = 4; + + // Similar to own_metrics, but for logs. + TelemetryConnectionSettings own_logs = 5; + + // Another set of connection settings, with a string name associated with each. + // How the Agent uses these is Agent-specific. Typically the name represents + // the name of the destination to connect to (as it is known to the Agent). + // If this field is not set then the Agent should assume that the other_connections + // settings are unchanged. + map other_connections = 6; +} + +// List of packages that the Server offers to the Agent. +// Status: [Beta] +message PackagesAvailable +{ + // Map of packages. Keys are package names, values are the packages available for download. + map packages = 1; + + // Aggregate hash of all remotely installed packages. The Agent SHOULD include this + // value in subsequent PackageStatuses messages. This in turn allows the management + // Server to identify that a different set of packages is available for the Agent + // and specify the available packages in the next ServerToAgent message. + // + // This field MUST be always set if the management Server supports packages + // of agents. + // + // The hash is calculated as an aggregate of all packages names and content. + bytes all_packages_hash = 2; +} + +// Each Agent is composed of one or more packages. A package has a name and +// content stored in a file. The content of the files, functionality +// provided by the packages, how they are stored and used by the Agent side is Agent +// type-specific and is outside the concerns of the OpAMP protocol. +// +// If the Agent does not have an installed package with the specified name then +// it SHOULD download it from the specified URL and install it. +// +// If the Agent already has an installed package with the specified name +// but with a different hash then the Agent SHOULD download and +// install the package again, since it is a different version of the same package. +// +// If the Agent has an installed package with the specified name and the same +// hash then the Agent does not need to do anything, it already +// has the right version of the package. +// Status: [Beta] +message PackageAvailable +{ + PackageType type = 1; + + // The package version that is available on the Server side. The Agent may for + // example use this information to avoid downloading a package that was previously + // already downloaded and failed to install. + string version = 2; + + // The downloadable file of the package. + DownloadableFile file = 3; + + // The hash of the package. SHOULD be calculated based on all other fields of the + // PackageAvailable message and content of the file of the package. The hash is + // used by the Agent to determine if the package it has is different from the + // package the Server is offering. + bytes hash = 4; +} + +// The type of the package, either an addon or a top-level package. +// Status: [Beta] +enum PackageType +{ + PackageType_TopLevel = 0; + PackageType_Addon = 1; +} + +// Status: [Beta] +message DownloadableFile +{ + // The URL from which the file can be downloaded using HTTP GET request. + // The Server at the specified URL SHOULD support range requests + // to allow for resuming downloads. + string download_url = 1; + + // The hash of the file content. Can be used by the Agent to verify that the file + // was downloaded correctly. + bytes content_hash = 2; + + // Optional signature of the file content. Can be used by the Agent to verify the + // authenticity of the downloaded file, for example can be the + // [detached GPG signature](https://www.gnupg.org/gph/en/manual/x135.html#AEN160). + // The exact signing and verification method is Agent specific. See + // https://github.com/open-telemetry/opamp-spec/blob/main/specification.md#code-signing + // for recommendations. + bytes signature = 3; + + // Optional headers to use when downloading a file. Typically used to set + // access tokens or other authorization headers. For HTTP-based protocols + // the Agent should set these in the request headers. + // For example: + // key="Authorization", Value="Basic YWxhZGRpbjpvcGVuc2VzYW1l". + // Status: [Development] + Headers headers = 4; +} + +message ServerErrorResponse +{ + ServerErrorResponseType type = 1; + + // Error message in the string form, typically human readable. + string error_message = 2; + + oneof Details + { + // Additional information about retrying if type==UNAVAILABLE. + RetryInfo retry_info = 3; + } +} + +enum ServerErrorResponseType +{ + // Unknown error. Something went wrong, but it is not known what exactly. + // The Agent SHOULD NOT retry the message. + // The error_message field may contain a description of the problem. + ServerErrorResponseType_Unknown = 0; + + // The AgentToServer message was malformed. The Agent SHOULD NOT retry + // the message. + ServerErrorResponseType_BadRequest = 1; + + // The Server is overloaded and unable to process the request. The Agent + // should retry the message later. retry_info field may be optionally + // set with additional information about retrying. + ServerErrorResponseType_Unavailable = 2; +} + +message RetryInfo +{ + uint64 retry_after_nanoseconds = 1; +} + +// ServerToAgentCommand is sent from the Server to the Agent to request that the Agent +// perform a command. +// Status: [Beta] +message ServerToAgentCommand +{ + CommandType type = 1; +} + +// Status: [Beta] +enum CommandType +{ + // The Agent should restart. This request will be ignored if the Agent does not + // support restart. + CommandType_Restart = 0; +} + +//////////////////////////////////////////////////////////////////////////////////// +// Status reporting + +message AgentDescription +{ + // Attributes that identify the Agent. + // Keys/values are according to OpenTelemetry semantic conventions, see: + // https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/resource/semantic_conventions + // + // For standalone running Agents (such as OpenTelemetry Collector) the following + // attributes SHOULD be specified: + // - service.name should be set to a reverse FQDN that uniquely identifies the + // Agent type, e.g. "io.opentelemetry.collector" + // - service.namespace if it is used in the environment where the Agent runs. + // - service.version should be set to version number of the Agent build. + // - service.instance.id should be set. It may be set equal to the Agent's + // instance uid (equal to ServerToAgent.instance_uid field) or any other value + // that uniquely identifies the Agent in combination with other attributes. + // - any other attributes that are necessary for uniquely identifying the Agent's + // own telemetry. + // + // The Agent SHOULD also include these attributes in the Resource of its own + // telemetry. The combination of identifying attributes SHOULD be sufficient to + // uniquely identify the Agent's own telemetry in the destination system to which + // the Agent sends its own telemetry. + repeated KeyValue identifying_attributes = 1; + + // Attributes that do not necessarily identify the Agent but help describe + // where it runs. + // The following attributes SHOULD be included: + // - os.type, os.version - to describe where the Agent runs. + // - host.* to describe the host the Agent runs on. + // - cloud.* to describe the cloud where the host is located. + // - any other relevant Resource attributes that describe this Agent and the + // environment it runs in. + // - any user-defined attributes that the end user would like to associate + // with this Agent. + repeated KeyValue non_identifying_attributes = 2; + + // TODO: add ability to specify related entities (such as the Service the Agent is + // is responsible/associated with). +} + +enum AgentCapabilities +{ + // The capabilities field is unspecified. + AgentCapabilities_Unspecified = 0; + // The Agent can report status. This bit MUST be set, since all Agents MUST + // report status. + AgentCapabilities_ReportsStatus = 0x00000001; + // The Agent can accept remote configuration from the Server. + AgentCapabilities_AcceptsRemoteConfig = 0x00000002; + // The Agent will report EffectiveConfig in AgentToServer. + AgentCapabilities_ReportsEffectiveConfig = 0x00000004; + // The Agent can accept package offers. + // Status: [Beta] + AgentCapabilities_AcceptsPackages = 0x00000008; + // The Agent can report package status. + // Status: [Beta] + AgentCapabilities_ReportsPackageStatuses = 0x00000010; + // The Agent can report own trace to the destination specified by + // the Server via ConnectionSettingsOffers.own_traces field. + // Status: [Beta] + AgentCapabilities_ReportsOwnTraces = 0x00000020; + // The Agent can report own metrics to the destination specified by + // the Server via ConnectionSettingsOffers.own_metrics field. + // Status: [Beta] + AgentCapabilities_ReportsOwnMetrics = 0x00000040; + // The Agent can report own logs to the destination specified by + // the Server via ConnectionSettingsOffers.own_logs field. + // Status: [Beta] + AgentCapabilities_ReportsOwnLogs = 0x00000080; + // The can accept connections settings for OpAMP via + // ConnectionSettingsOffers.opamp field. + // Status: [Beta] + AgentCapabilities_AcceptsOpAMPConnectionSettings = 0x00000100; + // The can accept connections settings for other destinations via + // ConnectionSettingsOffers.other_connections field. + // Status: [Beta] + AgentCapabilities_AcceptsOtherConnectionSettings = 0x00000200; + // The Agent can accept restart requests. + // Status: [Beta] + AgentCapabilities_AcceptsRestartCommand = 0x00000400; + // The Agent will report Health via AgentToServer.health field. + AgentCapabilities_ReportsHealth = 0x00000800; + // The Agent will report RemoteConfig status via AgentToServer.remote_config_status field. + AgentCapabilities_ReportsRemoteConfig = 0x00001000; + // The Agent can report heartbeats. + // This is specified by the ServerToAgent.OpAMPConnectionSettings.heartbeat_interval_seconds field. + // If this capability is true, but the Server does not set a heartbeat_interval_seconds field, the + // Agent should use its own configured interval, which by default will be 30s. The Server may not + // know the configured interval and should not make assumptions about it. + // Status: [Development] + AgentCapabilities_ReportsHeartbeat = 0x00002000; + // The agent will report AvailableComponents via the AgentToServer.available_components field. + // Status: [Development] + AgentCapabilities_ReportsAvailableComponents = 0x00004000; + // Add new capabilities here, continuing with the least significant unused bit. +} + +// The health of the Agent and sub-components +// Status: [Beta] +message ComponentHealth +{ + // Set to true if the component is up and healthy. + bool healthy = 1; + + // Timestamp since the component is up, i.e. when the component was started. + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + // If the component is not running MUST be set to 0. + fixed64 start_time_unix_nano = 2; + + // Human-readable error message if the component is in erroneous state. SHOULD be set + // when healthy==false. + string last_error = 3; + + // Component status represented as a string. The status values are defined by agent-specific + // semantics and not at the protocol level. + string status = 4; + + // The time when the component status was observed. Value is UNIX Epoch time in + // nanoseconds since 00:00:00 UTC on 1 January 1970. + fixed64 status_time_unix_nano = 5; + + // A map to store more granular, sub-component health. It can nest as deeply as needed to + // describe the underlying system. + map component_health_map = 6; +} + +message EffectiveConfig +{ + // The effective config of the Agent. + AgentConfigMap config_map = 1; +} + +message RemoteConfigStatus +{ + // The hash of the remote config that was last received by this Agent in the + // AgentRemoteConfig.config_hash field. + // The Server SHOULD compare this hash with the config hash + // it has for the Agent and if the hashes are different the Server MUST include + // the remote_config field in the response in the ServerToAgent message. + bytes last_remote_config_hash = 1; + + RemoteConfigStatuses status = 2; + + // Optional error message if status==FAILED. + string error_message = 3; +} + +enum RemoteConfigStatuses +{ + // The value of status field is not set. + RemoteConfigStatuses_UNSET = 0; + + // Remote config was successfully applied by the Agent. + RemoteConfigStatuses_APPLIED = 1; + + // Agent is currently applying the remote config that it received earlier. + RemoteConfigStatuses_APPLYING = 2; + + // Agent tried to apply the config received earlier, but it failed. + // See error_message for more details. + RemoteConfigStatuses_FAILED = 3; +} + +// The PackageStatuses message describes the status of all packages that the Agent +// has or was offered. +// Status: [Beta] +message PackageStatuses +{ + // A map of PackageStatus messages, where the keys are package names. + // The key MUST match the name field of PackageStatus message. + map packages = 1; + + // The aggregate hash of all packages that this Agent previously received from the + // Server via PackagesAvailable message. + // + // The Server SHOULD compare this hash to the aggregate hash of all packages that + // it has for this Agent and if the hashes are different the Server SHOULD send + // an PackagesAvailable message to the Agent. + bytes server_provided_all_packages_hash = 2; + + // This field is set if the Agent encountered an error when processing the + // PackagesAvailable message and that error is not related to any particular single + // package. + // The field must be unset is there were no processing errors. + string error_message = 3; +} + +// The status of a single package. +// Status: [Beta] +message PackageStatus +{ + // Package name. MUST be always set and MUST match the key in the packages field + // of PackageStatuses message. + string name = 1; + + // The version of the package that the Agent has. + // MUST be set if the Agent has this package. + // MUST be empty if the Agent does not have this package. This may be the case + // for example if the package was offered by the Server but failed to install + // and the Agent did not have this package previously. + string agent_has_version = 2; + + // The hash of the package that the Agent has. + // MUST be set if the Agent has this package. + // MUST be empty if the Agent does not have this package. This may be the case for + // example if the package was offered by the Server but failed to install and the + // Agent did not have this package previously. + bytes agent_has_hash = 3; + + // The version of the package that the Server offered to the Agent. + // MUST be set if the installation of the package is initiated by an earlier offer + // from the Server to install this package. + // + // MUST be empty if the Agent has this package but it was installed locally and + // was not offered by the Server. + // + // Note that it is possible for both agent_has_version and server_offered_version + // fields to be set and to have different values. This is for example possible if + // the Agent already has a version of the package successfully installed, the Server + // offers a different version, but the Agent fails to install that version. + string server_offered_version = 4; + + // The hash of the package that the Server offered to the Agent. + // MUST be set if the installation of the package is initiated by an earlier + // offer from the Server to install this package. + // + // MUST be empty if the Agent has this package but it was installed locally and + // was not offered by the Server. + // + // Note that it is possible for both agent_has_hash and server_offered_hash + // fields to be set and to have different values. This is for example possible if + // the Agent already has a version of the package successfully installed, the + // Server offers a different version, but the Agent fails to install that version. + bytes server_offered_hash = 5; + + PackageStatusEnum status = 6; + + // Error message if the status is erroneous. + string error_message = 7; + + // Optional details that may be of interest to a user. + // Should only be set if status is Downloading. + // Status: [Development] + PackageDownloadDetails download_details = 8; +} + + +// Additional details that an agent can use to describe an in-progress package download. +// Status: [Development] +message PackageDownloadDetails +{ + // The package download progress as a percentage. + double download_percent = 1; + + // The current package download rate in bytes per second. + double download_bytes_per_second = 2; +} + +// The status of this package. +// Status: [Beta] +enum PackageStatusEnum +{ + // Package is successfully installed by the Agent. + // The error_message field MUST NOT be set. + PackageStatusEnum_Installed = 0; + + // Installation of this package has not yet started. + PackageStatusEnum_InstallPending = 1; + + // Agent is currently installing the package. + // server_offered_hash field MUST be set to indicate the version that the + // Agent is installing. The error_message field MUST NOT be set. + PackageStatusEnum_Installing = 2; + + // Agent tried to install the package but installation failed. + // server_offered_hash field MUST be set to indicate the version that the Agent + // tried to install. The error_message may also contain more details about + // the failure. + PackageStatusEnum_InstallFailed = 3; + + // Agent is currently downloading the package. + // server_offered_hash field MUST be set to indicate the version that the + // Agent is installing. The error_message field MUST NOT be set. + // Status: [Development] + PackageStatusEnum_Downloading = 4; +} + +// Properties related to identification of the Agent, which can be overridden +// by the Server if needed +message AgentIdentification +{ + // When new_instance_uid is set, Agent MUST update instance_uid + // to the value provided and use it for all further communication. + // MUST be 16 bytes long and SHOULD be generated using the UUID v7 spec. + bytes new_instance_uid = 1; +} + +///////////////////////////////////////////////////////////////////////////////////// +// Config messages +///////////////////////////////////////////////////////////////////////////////////// + +message AgentRemoteConfig +{ + // Agent config offered by the management Server to the Agent instance. SHOULD NOT be + // set if the config for this Agent has not changed since it was last requested (i.e. + // AgentConfigRequest.last_remote_config_hash field is equal to + // AgentConfigResponse.config_hash field). + AgentConfigMap config = 1; + + // Hash of "config". The Agent SHOULD include this value in subsequent + // RemoteConfigStatus messages in the last_remote_config_hash field. This in turn + // allows the management Server to identify that a new config is available for the Agent. + // + // This field MUST be always set if the management Server supports remote configuration + // of agents. + // + // Management Server must choose a hashing function that guarantees lack of hash + // collisions in practice. + bytes config_hash = 2; +} + +message AgentConfigMap +{ + // Map of configs. Keys are config file names or config section names. + // The configuration is assumed to be a collection of one or more named config files + // or sections. + // For agents that use a single config file or section the map SHOULD contain a single + // entry and the key may be an empty string. + map config_map = 1; +} + +message AgentConfigFile +{ + // Config file or section body. The content, format and encoding depends on the Agent + // type. The content_type field may optionally describe the MIME type of the body. + bytes body = 1; + + // Optional MIME Content-Type that describes what's in the body field, for + // example "text/yaml". + string content_type = 2; +} + +///////////////////////////////////////////////////////////////////////////////////// +// Custom messages +///////////////////////////////////////////////////////////////////////////////////// + +message CustomCapabilities +{ + // A list of custom capabilities that are supported. Each capability is a reverse FQDN + // with optional version information that uniquely identifies the custom capability + // and should match a capability specified in a supported CustomMessage. + // Status: [Development] + repeated string capabilities = 1; +} + +message CustomMessage +{ + // A reverse FQDN that uniquely identifies the capability and matches one of the + // capabilities in the CustomCapabilities message. + // Status: [Development] + string capability = 1; + + // Type of message within the capability. The capability defines the types of custom + // messages that are used to implement the capability. The type must only be unique + // within the capability. + // Status: [Development] + string type = 2; + + // Binary data of the message. The capability must specify the format of the contents + // of the data for each custom message type it defines. + // Status: [Development] + bytes data = 3; +} diff --git a/src/OpenTelemetry.OpAmp.Client/Transport/Http/PlainHttpTransport.cs b/src/OpenTelemetry.OpAmp.Client/Transport/Http/PlainHttpTransport.cs new file mode 100644 index 0000000000..c00e750731 --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Transport/Http/PlainHttpTransport.cs @@ -0,0 +1,67 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NETFRAMEWORK +using System.Net.Http; +#endif + +using Google.Protobuf; +using OpenTelemetry.Internal; +using OpenTelemetry.OpAmp.Client.Utils; + +namespace OpenTelemetry.OpAmp.Client.Transport.Http; + +internal sealed class PlainHttpTransport : IOpAmpTransport, IDisposable +{ + private readonly Uri uri; + private readonly HttpClient httpClient; + private readonly HttpClientHandler handler; + private readonly FrameProcessor processor; + + public PlainHttpTransport(Uri serverUrl, FrameProcessor processor) + { + Guard.ThrowIfNull(serverUrl, nameof(serverUrl)); + Guard.ThrowIfNull(processor, nameof(processor)); + + this.uri = serverUrl; + this.processor = processor; + this.handler = new HttpClientHandler + { + // Trust all certificates + ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true, + }; + + this.httpClient = new HttpClient(this.handler); + } + + public async Task SendAsync(T message, CancellationToken token) + where T : IMessage + { + var content = message.ToByteArray(); + + using var byteContent = new ByteArrayContent(content); + byteContent.Headers.Add("Content-Type", "application/x-protobuf"); + + var response = await this.httpClient + .PostAsync(this.uri, byteContent, cancellationToken: token) + .ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + var responseMessage = await response.Content +#if NET + .ReadAsByteArrayAsync(token) +#else + .ReadAsByteArrayAsync() +#endif + .ConfigureAwait(false); + + this.processor.OnServerFrame(responseMessage.AsSequence()); + } + + public void Dispose() + { + this.httpClient?.Dispose(); + this.handler?.Dispose(); + } +} diff --git a/src/OpenTelemetry.OpAmp.Client/Transport/IOpAmpTransport.cs b/src/OpenTelemetry.OpAmp.Client/Transport/IOpAmpTransport.cs new file mode 100644 index 0000000000..5534300e28 --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Transport/IOpAmpTransport.cs @@ -0,0 +1,12 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Google.Protobuf; + +namespace OpenTelemetry.OpAmp.Client.Transport; + +internal interface IOpAmpTransport +{ + Task SendAsync(T message, CancellationToken token) + where T : IMessage; +} diff --git a/src/OpenTelemetry.OpAmp.Client/Utils/SequenceHelper.cs b/src/OpenTelemetry.OpAmp.Client/Utils/SequenceHelper.cs new file mode 100644 index 0000000000..1594761452 --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Utils/SequenceHelper.cs @@ -0,0 +1,19 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Buffers; + +namespace OpenTelemetry.OpAmp.Client.Utils; + +internal static class SequenceHelper +{ + public static ReadOnlySequence AsSequence(this byte[] message) + { + if (message == null || message.Length == 0) + { + return ReadOnlySequence.Empty; + } + + return new ReadOnlySequence(message); + } +} diff --git a/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj b/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj index 1578f748b0..c7eca052f9 100644 --- a/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj +++ b/test/OpenTelemetry.AotCompatibility.TestApp/OpenTelemetry.AotCompatibility.TestApp.csproj @@ -36,7 +36,6 @@ - diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/FrameBuilderTests.cs b/test/OpenTelemetry.OpAmp.Client.Tests/FrameBuilderTests.cs new file mode 100644 index 0000000000..677481912c --- /dev/null +++ b/test/OpenTelemetry.OpAmp.Client.Tests/FrameBuilderTests.cs @@ -0,0 +1,54 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Xunit; + +namespace OpenTelemetry.OpAmp.Client.Tests; + +public class FrameBuilderTests +{ + [Fact] + public void FrameBuilder_InitializesCorrectly() + { + var frameBuilder = new FrameBuilder(new()); + + var frame = frameBuilder + .StartBaseMessage() + .Build(); + + Assert.NotNull(frame); + Assert.NotEmpty(frame.InstanceUid); + Assert.Equal(1UL, frame.SequenceNum); + } + + [Fact] + public void FrameBuilder_Sequence() + { + var frameBuilder = new FrameBuilder(new()); + + var frame1 = frameBuilder + .StartBaseMessage() + .Build(); + + var frame2 = frameBuilder + .StartBaseMessage() + .Build(); + + var frame3 = frameBuilder + .StartBaseMessage() + .Build(); + + Assert.Equal(1UL, frame1.SequenceNum); + Assert.Equal(2UL, frame2.SequenceNum); + Assert.Equal(3UL, frame3.SequenceNum); + } + + [Fact] + public void FrameBuilder_ThrowsOnDoubleStartBaseMessage() + { + var frameBuilder = new FrameBuilder(new()); + frameBuilder.StartBaseMessage(); + + Assert.Throws(frameBuilder.StartBaseMessage); + } +} diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/FrameDispatcherTests.cs b/test/OpenTelemetry.OpAmp.Client.Tests/FrameDispatcherTests.cs new file mode 100644 index 0000000000..025e5f5971 --- /dev/null +++ b/test/OpenTelemetry.OpAmp.Client.Tests/FrameDispatcherTests.cs @@ -0,0 +1,45 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NET + +using OpenTelemetry.OpAmp.Client.Tests.Mocks; +using Xunit; + +namespace OpenTelemetry.OpAmp.Client.Tests; + +public class FrameDispatcherTests +{ + [Fact] + public async Task FrameDispatcher_IsThreadSafe_WhenDispatchingConcurrently() + { + var transport = new MockTransport(); + var settings = new OpAmpClientSettings(); + var dispatcher = new FrameDispatcher(transport, settings); + + // Simulate concurrent dispatches + var taskCount = 20; // Number of concurrent tasks + + await Parallel.ForEachAsync(Enumerable.Range(0, taskCount), async (i, token) => + { + if (i % 2 == 0) + { + await dispatcher.DispatchServerFrameAsync(token); + } + else + { + await dispatcher.DispatchServerFrameAsync(token); + } + }); + + // Assert that all messages were sent without exceptions + Assert.Equal(taskCount, transport.Messages.Count); + + // Assert that sequence numbers are from 1 to N (no duplicates, no gaps) + var sequenceNumbers = transport.Messages + .Select(m => m.SequenceNum) + .ToArray(); + Assert.Equal(Enumerable.Range(1, taskCount).Select(i => (ulong)i), sequenceNumbers); + } +} +#endif diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/FrameProcessorTests.cs b/test/OpenTelemetry.OpAmp.Client.Tests/FrameProcessorTests.cs new file mode 100644 index 0000000000..c70a9a3316 --- /dev/null +++ b/test/OpenTelemetry.OpAmp.Client.Tests/FrameProcessorTests.cs @@ -0,0 +1,85 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.OpAmp.Client.Tests.Mocks; +using OpenTelemetry.OpAmp.Client.Tests.Tools; +using Xunit; + +namespace OpenTelemetry.OpAmp.Client.Tests; + +public class FrameProcessorTests +{ + [Fact] + public void FrameProcessor_Subscribe() + { + var processor = new FrameProcessor(); + var listener = new MockListener(); + var mockFrame = FrameGenerator.GenerateMockFrame(); + + processor.Subscribe(listener); + processor.OnServerFrame(mockFrame.Frame); + + Assert.Single(listener.Messages); + Assert.Equal(mockFrame.ExptectedContent, listener.Messages[0].CustomMessage.Data.ToStringUtf8()); + } + + [Fact] + public void FrameProcessor_Unsubscribe() + { + var processor = new FrameProcessor(); + var listener = new MockListener(); + var mockFrame = FrameGenerator.GenerateMockFrame(); + + processor.Subscribe(listener); + processor.OnServerFrame(mockFrame.Frame); + + Assert.Single(listener.Messages); + + processor.Unsubscribe(listener); + processor.OnServerFrame(mockFrame.Frame); + Assert.Single(listener.Messages); + } + + [Fact] + public async Task FrameProcessor_ThreadSafety() + { + var processor = new FrameProcessor(); + var listener = new MockListener(); + var mockFrame = FrameGenerator.GenerateMockFrame(); + int iterations = 1000; + var tasks = new List + { + // Task to repeatedly call OnServerFrame + Task.Run(() => + { + Parallel.For(0, iterations, i => + { + processor.OnServerFrame(mockFrame.Frame); + }); + }), + + // Task to repeatedly subscribe + Task.Run(() => + { + Parallel.For(0, iterations, i => + { + processor.Subscribe(listener); + }); + }), + + // Task to repeatedly unsubscribe + Task.Run(() => + { + Parallel.For(0, iterations, i => + { + processor.Unsubscribe(listener); + }); + }), + }; + + await Task.WhenAll(tasks); + + // After all operations, ensure no exceptions and listener.Messages is in a valid state + Assert.True(listener.Messages.Count >= 0); + } +} diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/Mocks/MockFrame.cs b/test/OpenTelemetry.OpAmp.Client.Tests/Mocks/MockFrame.cs new file mode 100644 index 0000000000..ac3992cf12 --- /dev/null +++ b/test/OpenTelemetry.OpAmp.Client.Tests/Mocks/MockFrame.cs @@ -0,0 +1,15 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Buffers; + +namespace OpenTelemetry.OpAmp.Client.Tests.Mocks; + +internal class MockFrame +{ + public ReadOnlySequence Frame { get; set; } + + public bool HasHeader { get; set; } + + public string? ExptectedContent { get; set; } +} diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/Mocks/MockListener.cs b/test/OpenTelemetry.OpAmp.Client.Tests/Mocks/MockListener.cs new file mode 100644 index 0000000000..dd462dc806 --- /dev/null +++ b/test/OpenTelemetry.OpAmp.Client.Tests/Mocks/MockListener.cs @@ -0,0 +1,14 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.OpAmp.Client.Listeners; +using OpenTelemetry.OpAmp.Client.Listeners.Messages; + +namespace OpenTelemetry.OpAmp.Client.Tests.Mocks; + +internal class MockListener : IOpAmpListener +{ + public List Messages { get; private set; } = []; + + public void HandleMessage(CustomMessageMessage message) => this.Messages.Add(message); +} diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/Mocks/MockTransport.cs b/test/OpenTelemetry.OpAmp.Client.Tests/Mocks/MockTransport.cs new file mode 100644 index 0000000000..78805e72b8 --- /dev/null +++ b/test/OpenTelemetry.OpAmp.Client.Tests/Mocks/MockTransport.cs @@ -0,0 +1,30 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Google.Protobuf; +using OpAmp.Proto.V1; +using OpenTelemetry.OpAmp.Client.Transport; + +namespace OpenTelemetry.OpAmp.Client.Tests.Mocks; + +internal class MockTransport : IOpAmpTransport +{ + private readonly List messages = []; + + public IReadOnlyCollection Messages => this.messages.AsReadOnly(); + + public Task SendAsync(T message, CancellationToken token) + where T : IMessage + { + if (message is AgentToServer agentToServer) + { + this.messages.Add(agentToServer); + } + else + { + throw new InvalidOperationException("Unsupported message type. Only AgentToServer messages are supported."); + } + + return Task.CompletedTask; + } +} diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/OpenTelemetry.OpAmp.Client.Tests.csproj b/test/OpenTelemetry.OpAmp.Client.Tests/OpenTelemetry.OpAmp.Client.Tests.csproj index 66d712cc90..981f682793 100644 --- a/test/OpenTelemetry.OpAmp.Client.Tests/OpenTelemetry.OpAmp.Client.Tests.csproj +++ b/test/OpenTelemetry.OpAmp.Client.Tests/OpenTelemetry.OpAmp.Client.Tests.csproj @@ -6,4 +6,12 @@ Unit test project for OpenTelemetry OpAMP Client. + + + + + + + + diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/PlainHttpTransportTest.cs b/test/OpenTelemetry.OpAmp.Client.Tests/PlainHttpTransportTest.cs new file mode 100644 index 0000000000..36111cb6a7 --- /dev/null +++ b/test/OpenTelemetry.OpAmp.Client.Tests/PlainHttpTransportTest.cs @@ -0,0 +1,44 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Google.Protobuf; +using OpAmp.Proto.V1; +using OpenTelemetry.OpAmp.Client.Tests.Mocks; +using OpenTelemetry.OpAmp.Client.Tests.Tools; +using OpenTelemetry.OpAmp.Client.Transport.Http; +using Xunit; + +namespace OpenTelemetry.OpAmp.Client.Tests; + +public class PlainHttpTransportTest +{ + [Fact] + public async Task PlainHttpTransport_SendReceiveCommunication() + { + // Arrange + using var opAmpServer = new OpAmpFakeHttpServer(); + var opAmpEndpoint = opAmpServer.Endpoint; + + var mockListener = new MockListener(); + var frameProcessor = new FrameProcessor(); + frameProcessor.Subscribe(mockListener); + + var httpTransport = new PlainHttpTransport(opAmpEndpoint, frameProcessor); + + var uid = ByteString.CopyFrom(Guid.NewGuid().ToByteArray()); + var frame = new AgentToServer() { InstanceUid = uid }; + + // Act + await httpTransport.SendAsync(frame, CancellationToken.None); + + // Assert + var serverReceivedFrames = opAmpServer.GetFrames(); + var clientReceivedFrames = mockListener.Messages; + + Assert.Single(serverReceivedFrames); + Assert.Equal(uid, serverReceivedFrames.First().InstanceUid); + + Assert.Single(clientReceivedFrames); + Assert.Equal("Response from OpAmpFakeHttpServer", clientReceivedFrames.First().CustomMessage.Data.ToStringUtf8()); + } +} diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/Tools/FrameGenerator.cs b/test/OpenTelemetry.OpAmp.Client.Tests/Tools/FrameGenerator.cs new file mode 100644 index 0000000000..ef8e9768e6 --- /dev/null +++ b/test/OpenTelemetry.OpAmp.Client.Tests/Tools/FrameGenerator.cs @@ -0,0 +1,33 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Buffers; +using Google.Protobuf; +using OpAmp.Proto.V1; +using OpenTelemetry.OpAmp.Client.Tests.Mocks; + +namespace OpenTelemetry.OpAmp.Client.Tests.Tools; + +internal class FrameGenerator +{ + public static MockFrame GenerateMockFrame() + { + var content = "This is a mock frame for testing purposes."; + var frame = new ServerToAgent + { + InstanceUid = ByteString.CopyFrom(Guid.NewGuid().ToByteArray()), + CustomMessage = new CustomMessage() + { + Data = ByteString.CopyFromUtf8(content), + Type = "Utf8String", + }, + }; + + return new MockFrame + { + Frame = new ReadOnlySequence(frame.ToByteArray()), + HasHeader = false, + ExptectedContent = content, + }; + } +} diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/Tools/OpAmpFakeHttpServer.cs b/test/OpenTelemetry.OpAmp.Client.Tests/Tools/OpAmpFakeHttpServer.cs new file mode 100644 index 0000000000..ddb3a08299 --- /dev/null +++ b/test/OpenTelemetry.OpAmp.Client.Tests/Tools/OpAmpFakeHttpServer.cs @@ -0,0 +1,60 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Concurrent; +using System.Net; +using Google.Protobuf; +using OpAmp.Proto.V1; +using OpenTelemetry.Tests; + +namespace OpenTelemetry.OpAmp.Client.Tests.Tools; + +internal class OpAmpFakeHttpServer : IDisposable +{ + private readonly IDisposable httpServer; + private readonly BlockingCollection frames; + + public OpAmpFakeHttpServer() + { + this.frames = []; + this.httpServer = TestHttpServer.RunServer( + context => + { + var buffer = new byte[context.Request.ContentLength64]; + _ = context.Request.InputStream.Read(buffer, 0, buffer.Length); + var frame = AgentToServer.Parser.ParseFrom(buffer); + + this.frames.Add(frame); + + var response = new ServerToAgent + { + InstanceUid = frame.InstanceUid, + CustomMessage = new CustomMessage + { + Data = ByteString.CopyFromUtf8("Response from OpAmpFakeHttpServer"), + }, + }; + var responseBytes = response.ToByteArray(); + + context.Response.StatusCode = (int)HttpStatusCode.OK; + context.Response.ContentType = "application/x-protobuf"; + context.Response.OutputStream.Write(responseBytes, 0, responseBytes.Length); + context.Response.Close(); + }, + out var host, + out var port); + this.Endpoint = new Uri($"http://{host}:{port}"); + } + + public Uri Endpoint { get; } + + public IReadOnlyCollection GetFrames() + { + return this.frames.ToArray(); + } + + public void Dispose() + { + this.httpServer.Dispose(); + } +}