//
// Copyright The 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.
//
#if NETCOREAPP3_1
using System;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using OpenTelemetry.Context;
using OpenTelemetry.Context.Propagation;
using OpenTelemetry.Instrumentation.Http.Implementation;
using OpenTelemetry.Tests;
using OpenTelemetry.Trace;
using Xunit;
namespace OpenTelemetry.Instrumentation.Http.Tests
{
public partial class HttpClientTests : IDisposable
{
private readonly IDisposable serverLifeTime;
private readonly string url;
public HttpClientTests()
{
this.serverLifeTime = TestHttpServer.RunServer(
(ctx) =>
{
ctx.Response.StatusCode = 200;
ctx.Response.OutputStream.Close();
},
out var host,
out var port);
this.url = $"http://{host}:{port}/";
}
[Fact]
public void AddHttpClientInstrumentation_BadArgs()
{
TracerProviderBuilder builder = null;
Assert.Throws(() => builder.AddHttpClientInstrumentation());
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task HttpClientInstrumentationInjectsHeadersAsync(bool shouldEnrich)
{
var processor = new Mock>();
var request = new HttpRequestMessage
{
RequestUri = new Uri(this.url),
Method = new HttpMethod("GET"),
};
var parent = new Activity("parent")
.SetIdFormat(ActivityIdFormat.W3C)
.Start();
parent.TraceStateString = "k1=v1,k2=v2";
parent.ActivityTraceFlags = ActivityTraceFlags.Recorded;
// Ensure that the header value func does not throw if the header key can't be found
var mockPropagator = new Mock();
// var isInjectedHeaderValueGetterThrows = false;
// mockTextFormat
// .Setup(x => x.IsInjected(It.IsAny(), It.IsAny>>()))
// .Callback>>(
// (carrier, getter) =>
// {
// try
// {
// // traceparent doesn't exist
// getter(carrier, "traceparent");
// }
// catch
// {
// isInjectedHeaderValueGetterThrows = true;
// }
// });
using (Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation(o =>
{
o.Propagator = mockPropagator.Object;
if (shouldEnrich)
{
o.Enrich = ActivityEnrichment;
}
})
.AddProcessor(processor.Object)
.Build())
{
using var c = new HttpClient();
await c.SendAsync(request);
}
Assert.Equal(4, processor.Invocations.Count); // OnStart/OnEnd/OnShutdown/Dispose called.
var activity = (Activity)processor.Invocations[1].Arguments[0];
Assert.Equal(ActivityKind.Client, activity.Kind);
Assert.Equal(parent.TraceId, activity.Context.TraceId);
Assert.Equal(parent.SpanId, activity.ParentSpanId);
Assert.NotEqual(parent.SpanId, activity.Context.SpanId);
Assert.NotEqual(default, activity.Context.SpanId);
Assert.True(request.Headers.TryGetValues("traceparent", out var traceparents));
Assert.True(request.Headers.TryGetValues("tracestate", out var tracestates));
Assert.Single(traceparents);
Assert.Single(tracestates);
Assert.Equal($"00-{activity.Context.TraceId}-{activity.Context.SpanId}-01", traceparents.Single());
Assert.Equal("k1=v1,k2=v2", tracestates.Single());
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task HttpClientInstrumentationInjectsHeadersAsync_CustomFormat(bool shouldEnrich)
{
var propagator = new Mock();
propagator.Setup(m => m.Inject(It.IsAny(), It.IsAny(), It.IsAny>()))
.Callback>((context, message, action) =>
{
action(message, "custom_traceparent", $"00/{context.ActivityContext.TraceId}/{context.ActivityContext.SpanId}/01");
action(message, "custom_tracestate", Activity.Current.TraceStateString);
});
var processor = new Mock>();
var request = new HttpRequestMessage
{
RequestUri = new Uri(this.url),
Method = new HttpMethod("GET"),
};
var parent = new Activity("parent")
.SetIdFormat(ActivityIdFormat.W3C)
.Start();
parent.TraceStateString = "k1=v1,k2=v2";
parent.ActivityTraceFlags = ActivityTraceFlags.Recorded;
using (Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation((opt) =>
{
opt.Propagator = propagator.Object;
if (shouldEnrich)
{
opt.Enrich = ActivityEnrichment;
}
})
.AddProcessor(processor.Object)
.Build())
{
using var c = new HttpClient();
await c.SendAsync(request);
}
Assert.Equal(4, processor.Invocations.Count); // OnStart/OnEnd/OnShutdown/Dispose called.
var activity = (Activity)processor.Invocations[1].Arguments[0];
Assert.Equal(ActivityKind.Client, activity.Kind);
Assert.Equal(parent.TraceId, activity.Context.TraceId);
Assert.Equal(parent.SpanId, activity.ParentSpanId);
Assert.NotEqual(parent.SpanId, activity.Context.SpanId);
Assert.NotEqual(default, activity.Context.SpanId);
Assert.True(request.Headers.TryGetValues("custom_traceparent", out var traceparents));
Assert.True(request.Headers.TryGetValues("custom_tracestate", out var tracestates));
Assert.Single(traceparents);
Assert.Single(tracestates);
Assert.Equal($"00/{activity.Context.TraceId}/{activity.Context.SpanId}/01", traceparents.Single());
Assert.Equal("k1=v1,k2=v2", tracestates.Single());
}
[Fact]
public async Task HttpClientInstrumentation_AddViaFactory_HttpInstrumentation_CollectsSpans()
{
var processor = new Mock>();
using (Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation()
.AddProcessor(processor.Object)
.Build())
{
using var c = new HttpClient();
await c.GetAsync(this.url);
}
Assert.Single(processor.Invocations.Where(i => i.Method.Name == "OnStart"));
Assert.Single(processor.Invocations.Where(i => i.Method.Name == "OnEnd"));
Assert.IsType(processor.Invocations[1].Arguments[0]);
}
[Fact]
public async Task HttpClientInstrumentation_AddViaFactory_DependencyInstrumentation_CollectsSpans()
{
var processor = new Mock>();
using (Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation()
.AddProcessor(processor.Object)
.Build())
{
using var c = new HttpClient();
await c.GetAsync(this.url);
}
Assert.Single(processor.Invocations.Where(i => i.Method.Name == "OnStart"));
Assert.Single(processor.Invocations.Where(i => i.Method.Name == "OnEnd"));
Assert.IsType(processor.Invocations[1].Arguments[0]);
}
[Fact]
public async Task HttpClientInstrumentationBacksOffIfAlreadyInstrumented()
{
var processor = new Mock>();
var request = new HttpRequestMessage
{
RequestUri = new Uri(this.url),
Method = new HttpMethod("GET"),
};
request.Headers.Add("traceparent", "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01");
using (Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation()
.AddProcessor(processor.Object)
.Build())
{
using var c = new HttpClient();
await c.SendAsync(request);
}
Assert.Equal(2, processor.Invocations.Count); // OnShutdown/Dispose called.
}
[Fact]
public async void RequestNotCollectedWhenInstrumentationFilterApplied()
{
var processor = new Mock>();
using (Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation(
(opt) => opt.Filter = (req) => !req.RequestUri.OriginalString.Contains(this.url))
.AddProcessor(processor.Object)
.Build())
{
using var c = new HttpClient();
await c.GetAsync(this.url);
}
Assert.Equal(2, processor.Invocations.Count); // OnShutdown/Dispose called.
}
[Fact]
public async void RequestNotCollectedWhenInstrumentationFilterThrowsException()
{
var processor = new Mock>();
using (Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation(
(opt) => opt.Filter = (req) => throw new Exception("From InstrumentationFilter"))
.AddProcessor(processor.Object)
.Build())
{
using var c = new HttpClient();
using (var inMemoryEventListener = new InMemoryEventListener(HttpInstrumentationEventSource.Log))
{
await c.GetAsync(this.url);
Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 4));
}
}
Assert.Equal(2, processor.Invocations.Count); // OnShutdown/Dispose called.
}
[Fact]
public async Task HttpClientInstrumentationCorrelationAndBaggage()
{
var activityProcessor = new Mock>();
using var parent = new Activity("w3c activity");
parent.SetIdFormat(ActivityIdFormat.W3C);
parent.AddBaggage("k1", "v1");
parent.ActivityTraceFlags = ActivityTraceFlags.Recorded;
parent.Start();
Baggage.SetBaggage("k2", "v2");
using (Sdk.CreateTracerProviderBuilder()
.AddHttpClientInstrumentation(options => options.Enrich = ActivityEnrichment)
.AddProcessor(activityProcessor.Object)
.Build())
{
using var c = new HttpClient();
using var r = await c.GetAsync("https://opentelemetry.io/").ConfigureAwait(false);
}
Assert.Equal(4, activityProcessor.Invocations.Count);
}
public void Dispose()
{
this.serverLifeTime?.Dispose();
Activity.Current = null;
}
private static void ActivityEnrichment(Activity activity, string method, object obj)
{
switch (method)
{
case "OnStartActivity":
Assert.True(obj is HttpRequestMessage);
break;
case "OnStopActivity":
Assert.True(obj is HttpResponseMessage);
break;
case "OnException":
Assert.True(obj is Exception);
break;
default:
break;
}
}
}
}
#endif