diff --git a/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt index d33854b7ad..4c6f73fd75 100644 --- a/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt @@ -36,6 +36,8 @@ OpenTelemetry.OpAmp.Client.Settings.OpAmpClientSettings.ConnectionType.get -> Op OpenTelemetry.OpAmp.Client.Settings.OpAmpClientSettings.ConnectionType.set -> void OpenTelemetry.OpAmp.Client.Settings.OpAmpClientSettings.Heartbeat.get -> OpenTelemetry.OpAmp.Client.Settings.HeartbeatSettings! OpenTelemetry.OpAmp.Client.Settings.OpAmpClientSettings.Heartbeat.set -> void +OpenTelemetry.OpAmp.Client.Settings.OpAmpClientSettings.HttpClientFactory.get -> System.Func! +OpenTelemetry.OpAmp.Client.Settings.OpAmpClientSettings.HttpClientFactory.set -> void OpenTelemetry.OpAmp.Client.Settings.OpAmpClientSettings.Identification.get -> OpenTelemetry.OpAmp.Client.Settings.IdentificationSettings! OpenTelemetry.OpAmp.Client.Settings.OpAmpClientSettings.Identification.set -> void OpenTelemetry.OpAmp.Client.Settings.OpAmpClientSettings.InstanceUid.get -> System.Guid diff --git a/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md b/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md index 410d610ce2..1965d2c5c4 100644 --- a/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md +++ b/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +* 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)) + ## 0.1.0-alpha.3 Released 2025-Nov-13 diff --git a/src/OpenTelemetry.OpAmp.Client/Internal/Transport/Http/PlainHttpTransport.cs b/src/OpenTelemetry.OpAmp.Client/Internal/Transport/Http/PlainHttpTransport.cs index 6b7ad0c725..545b6a90a6 100644 --- a/src/OpenTelemetry.OpAmp.Client/Internal/Transport/Http/PlainHttpTransport.cs +++ b/src/OpenTelemetry.OpAmp.Client/Internal/Transport/Http/PlainHttpTransport.cs @@ -8,6 +8,7 @@ using Google.Protobuf; using OpenTelemetry.Internal; using OpenTelemetry.OpAmp.Client.Internal.Utils; +using OpenTelemetry.OpAmp.Client.Settings; namespace OpenTelemetry.OpAmp.Client.Internal.Transport.Http; @@ -15,23 +16,16 @@ 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) + public PlainHttpTransport(OpAmpClientSettings settings, FrameProcessor processor) { - Guard.ThrowIfNull(serverUrl, nameof(serverUrl)); + Guard.ThrowIfNull(settings, nameof(settings)); Guard.ThrowIfNull(processor, nameof(processor)); - this.uri = serverUrl; + this.uri = settings.ServerUrl; this.processor = processor; - this.handler = new HttpClientHandler - { - // Trust all certificates - ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true, - }; - - this.httpClient = new HttpClient(this.handler); + this.httpClient = settings.HttpClientFactory(); } public async Task SendAsync(T message, CancellationToken token) @@ -62,6 +56,5 @@ public async Task SendAsync(T message, CancellationToken token) public void Dispose() { this.httpClient?.Dispose(); - this.handler?.Dispose(); } } diff --git a/src/OpenTelemetry.OpAmp.Client/OpAmpClient.cs b/src/OpenTelemetry.OpAmp.Client/OpAmpClient.cs index 9087c68d9c..e0b99c5c9d 100644 --- a/src/OpenTelemetry.OpAmp.Client/OpAmpClient.cs +++ b/src/OpenTelemetry.OpAmp.Client/OpAmpClient.cs @@ -93,7 +93,7 @@ private static IOpAmpTransport ConstructTransport(OpAmpClientSettings settings, return settings.ConnectionType switch { ConnectionType.WebSocket => new WsTransport(settings.ServerUrl, processor), - ConnectionType.Http => new PlainHttpTransport(settings.ServerUrl, processor), + ConnectionType.Http => new PlainHttpTransport(settings, processor), _ => throw new NotSupportedException("Unsupported transport type"), }; } diff --git a/src/OpenTelemetry.OpAmp.Client/Settings/OpAmpClientSettings.cs b/src/OpenTelemetry.OpAmp.Client/Settings/OpAmpClientSettings.cs index 363dee05ea..ab0178239b 100644 --- a/src/OpenTelemetry.OpAmp.Client/Settings/OpAmpClientSettings.cs +++ b/src/OpenTelemetry.OpAmp.Client/Settings/OpAmpClientSettings.cs @@ -1,6 +1,12 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#if NETFRAMEWORK +using System.Net.Http; +#endif + +using OpenTelemetry.Internal; + namespace OpenTelemetry.OpAmp.Client.Settings; /// @@ -8,6 +14,8 @@ namespace OpenTelemetry.OpAmp.Client.Settings; /// public sealed class OpAmpClientSettings { + private readonly Func defaultHttpClientFactory = () => new HttpClient(); + private Uri? serverUrl; /// @@ -91,4 +99,27 @@ public Uri ServerUrl /// Gets or sets the heartbeat settings. /// public HeartbeatSettings Heartbeat { get; set; } = new(); + + /// + /// Gets or sets the factory function called to create the instance that will be used at runtime to + /// transmit OpAmp messages over HTTP. The returned instance will + /// be reused for all communication. + /// + /// + /// Notes: + /// + /// This is only invoked for the protocol. + /// + /// + public Func HttpClientFactory + { + get => field ?? this.defaultHttpClientFactory; + set + { + Guard.ThrowIfNull(value); + field = value; + } + } } diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/PlainHttpTransportTest.cs b/test/OpenTelemetry.OpAmp.Client.Tests/PlainHttpTransportTest.cs index 49856e58cf..18aa1ac493 100644 --- a/test/OpenTelemetry.OpAmp.Client.Tests/PlainHttpTransportTest.cs +++ b/test/OpenTelemetry.OpAmp.Client.Tests/PlainHttpTransportTest.cs @@ -1,8 +1,13 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#if NETFRAMEWORK +using System.Net.Http; +#endif + using OpenTelemetry.OpAmp.Client.Internal; using OpenTelemetry.OpAmp.Client.Internal.Transport.Http; +using OpenTelemetry.OpAmp.Client.Settings; using OpenTelemetry.OpAmp.Client.Tests.Mocks; using OpenTelemetry.OpAmp.Client.Tests.Tools; using Xunit; @@ -19,12 +24,13 @@ public async Task PlainHttpTransport_SendReceiveCommunication(bool useSmallPacke // Arrange using var opAmpServer = new OpAmpFakeHttpServer(useSmallPackets); var opAmpEndpoint = opAmpServer.Endpoint; + var settings = new OpAmpClientSettings { ServerUrl = opAmpEndpoint }; using var mockListener = new MockListener(); var frameProcessor = new FrameProcessor(); frameProcessor.Subscribe(mockListener); - var httpTransport = new PlainHttpTransport(opAmpEndpoint, frameProcessor); + var httpTransport = new PlainHttpTransport(settings, frameProcessor); var mockFrame = FrameGenerator.GenerateMockAgentFrame(useSmallPackets); @@ -42,4 +48,37 @@ public async Task PlainHttpTransport_SendReceiveCommunication(bool useSmallPacke Assert.Single(clientReceivedFrames); Assert.StartsWith("This is a mock server frame for testing purposes.", receivedTextData); } + + [Fact] + public async Task PlainHttpTransport_UsesConfiguredHttpClientFactory() + { + // Arrange + using var opAmpServer = new OpAmpFakeHttpServer(false); + var opAmpEndpoint = opAmpServer.Endpoint; + var settings = new OpAmpClientSettings + { + ServerUrl = opAmpEndpoint, + HttpClientFactory = () => + { + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("X-Custom-Header", "CustomValue"); + return client; + }, + }; + + using var mockListener = new MockListener(); + var frameProcessor = new FrameProcessor(); + frameProcessor.Subscribe(mockListener); + + var httpTransport = new PlainHttpTransport(settings, frameProcessor); + + var mockFrame = FrameGenerator.GenerateMockAgentFrame(false); + + // Act + await httpTransport.SendAsync(mockFrame.Frame, CancellationToken.None); + + // Assert + var serverReceivedHeaders = opAmpServer.GetHeaders(); + Assert.Contains(serverReceivedHeaders, headers => headers["X-Custom-Header"] == "CustomValue"); + } } diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/Tools/OpAmpFakeHttpServer.cs b/test/OpenTelemetry.OpAmp.Client.Tests/Tools/OpAmpFakeHttpServer.cs index 13bbb6f9bc..a90a0da868 100644 --- a/test/OpenTelemetry.OpAmp.Client.Tests/Tools/OpAmpFakeHttpServer.cs +++ b/test/OpenTelemetry.OpAmp.Client.Tests/Tools/OpAmpFakeHttpServer.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Collections.Concurrent; +using System.Collections.Specialized; using System.Net; using OpAmp.Proto.V1; using OpenTelemetry.Tests; @@ -12,6 +13,7 @@ internal class OpAmpFakeHttpServer : IDisposable { private readonly IDisposable httpServer; private readonly BlockingCollection frames = []; + private readonly List receivedHeaders = []; public OpAmpFakeHttpServer(bool useSmallPackets) { @@ -21,6 +23,14 @@ public OpAmpFakeHttpServer(bool useSmallPackets) var frame = ProcessReceive(context.Request); this.frames.Add(frame); + var headersCopy = new NameValueCollection(); + foreach (var key in context.Request.Headers.AllKeys) + { + headersCopy.Add(key, context.Request.Headers[key]); + } + + this.receivedHeaders.Add(headersCopy); + var response = GenerateResponse(frame, useSmallPackets); context.Response.StatusCode = (int)HttpStatusCode.OK; @@ -40,6 +50,11 @@ public IReadOnlyCollection GetFrames() return this.frames.ToArray(); } + public IReadOnlyList GetHeaders() + { + return this.receivedHeaders.AsReadOnly(); + } + public void Dispose() { this.httpServer.Dispose();