From 865520f2d755bc050c3d7352c90444cd6f0cb8d6 Mon Sep 17 00:00:00 2001 From: Tom Pallister Date: Mon, 25 May 2020 18:42:48 +0100 Subject: [PATCH] Feat/opentracing (#1243) --- Ocelot.sln | 17 + docs/features/tracing.rst | 37 +- .../OcelotOpenTracing.csproj | 30 + samples/OcelotOpenTracing/Program.cs | 65 +++ .../appsettings.Development.json | 9 + samples/OcelotOpenTracing/appsettings.json | 10 + samples/OcelotOpenTracing/ocelot.json | 24 + .../Ocelot.Tracing.OpenTracing.csproj | 21 + .../OcelotBuilderExtensions.cs | 15 + .../OpenTracingTracer.cs | 75 +++ .../Ocelot.AcceptanceTests.csproj | 150 ++--- .../OpenTracingTests.cs | 516 ++++++++++++++++++ test/Ocelot.AcceptanceTests/Steps.cs | 36 ++ 13 files changed, 929 insertions(+), 76 deletions(-) create mode 100644 samples/OcelotOpenTracing/OcelotOpenTracing.csproj create mode 100644 samples/OcelotOpenTracing/Program.cs create mode 100644 samples/OcelotOpenTracing/appsettings.Development.json create mode 100644 samples/OcelotOpenTracing/appsettings.json create mode 100644 samples/OcelotOpenTracing/ocelot.json create mode 100644 src/Ocelot.Tracing.OpenTracing/Ocelot.Tracing.OpenTracing.csproj create mode 100644 src/Ocelot.Tracing.OpenTracing/OcelotBuilderExtensions.cs create mode 100644 src/Ocelot.Tracing.OpenTracing/OpenTracingTracer.cs create mode 100644 test/Ocelot.AcceptanceTests/OpenTracingTests.cs diff --git a/Ocelot.sln b/Ocelot.sln index 203dfb921..511c79839 100644 --- a/Ocelot.sln +++ b/Ocelot.sln @@ -82,6 +82,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "basic", "basic", "{ED066001 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "graphql", "graphql", "{C15CD120-5F8D-41DE-9B21-00E3EA77D6C1}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Tracing.OpenTracing", "src\Ocelot.Tracing.OpenTracing\Ocelot.Tracing.OpenTracing.csproj", "{11C622AD-8C0A-4CF4-811B-3DBB76550797}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "open-tracing", "open-tracing", "{731C6A8A-69ED-445C-A132-C638AA93F9C7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OcelotOpenTracing", "samples\OcelotOpenTracing\OcelotOpenTracing.csproj", "{C9427E78-4281-4F59-A66E-17C0B66550E5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -180,6 +186,14 @@ Global {33BE6D88-F188-4E60-83AC-3C4B94D24675}.Debug|Any CPU.Build.0 = Debug|Any CPU {33BE6D88-F188-4E60-83AC-3C4B94D24675}.Release|Any CPU.ActiveCfg = Release|Any CPU {33BE6D88-F188-4E60-83AC-3C4B94D24675}.Release|Any CPU.Build.0 = Release|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11C622AD-8C0A-4CF4-811B-3DBB76550797}.Release|Any CPU.Build.0 = Release|Any CPU + {C9427E78-4281-4F59-A66E-17C0B66550E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9427E78-4281-4F59-A66E-17C0B66550E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9427E78-4281-4F59-A66E-17C0B66550E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9427E78-4281-4F59-A66E-17C0B66550E5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -214,6 +228,9 @@ Global {1F1F324D-6EA4-4E63-A6A7-C6053F412F1A} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} {ED066001-BAF7-4117-9884-DF591A56347D} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} {C15CD120-5F8D-41DE-9B21-00E3EA77D6C1} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {11C622AD-8C0A-4CF4-811B-3DBB76550797} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {731C6A8A-69ED-445C-A132-C638AA93F9C7} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {C9427E78-4281-4F59-A66E-17C0B66550E5} = {731C6A8A-69ED-445C-A132-C638AA93F9C7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48} diff --git a/docs/features/tracing.rst b/docs/features/tracing.rst index 5ff66882e..a400a8091 100644 --- a/docs/features/tracing.rst +++ b/docs/features/tracing.rst @@ -1,8 +1,41 @@ Tracing ======= -This page details how to perform distributed tracing with Ocelot. At the moment we only support Butterfly but other tracers might just work without -anything Ocelot specific. +This page details how to perform distributed tracing with Ocelot. + +OpenTracing +^^^^^^^^^^^ + +Ocelot providers tracing functionality from the excellent `OpenTracing C# `_ project. The code for the Ocelot integration +can be found `here `_. + +The example below uses `Jaeger C# `_ client to provide the tracer used in Ocelot. + +.. code-block:: csharp + + services.AddSingleton(sp => + { + var loggerFactory = sp.GetService(); + Configuration config = new Configuration(context.HostingEnvironment.ApplicationName, loggerFactory); + + var tracer = config.GetTracer(); + GlobalTracer.Register(tracer); + return tracer; + }); + + services + .AddOcelot() + .AddOpenTracing(); + +Then in your ocelot.json add the following to the Route you want to trace.. + +.. code-block:: json + + "HttpHandlerOptions": { + "UseTracing": true + }, + +Ocelot will now send tracing information to Jaeger when this Route is called. Butterfly ^^^^^^^^^ diff --git a/samples/OcelotOpenTracing/OcelotOpenTracing.csproj b/samples/OcelotOpenTracing/OcelotOpenTracing.csproj new file mode 100644 index 000000000..295fc461c --- /dev/null +++ b/samples/OcelotOpenTracing/OcelotOpenTracing.csproj @@ -0,0 +1,30 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + + + + + + true + + + true + + + true + + + + diff --git a/samples/OcelotOpenTracing/Program.cs b/samples/OcelotOpenTracing/Program.cs new file mode 100644 index 000000000..e9527930f --- /dev/null +++ b/samples/OcelotOpenTracing/Program.cs @@ -0,0 +1,65 @@ +namespace OcelotOpenTracing +{ + using Microsoft.AspNetCore.Hosting; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Hosting; + using System.IO; + using Ocelot.DependencyInjection; + using Ocelot.Middleware; + using Microsoft.Extensions.Logging; + using Ocelot.Tracing.OpenTracing; + using Jaeger; + using Microsoft.Extensions.DependencyInjection; + using OpenTracing; + using OpenTracing.Util; + + internal static class Program + { + private static void Main(string[] args) + { + Host.CreateDefaultBuilder() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseKestrel() + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", + optional: true, reloadOnChange: false) + .AddJsonFile("ocelot.json", optional: false, reloadOnChange: true) + .AddEnvironmentVariables(); + }) + .ConfigureServices((context, services) => + { + services.AddSingleton(sp => + { + var loggerFactory = sp.GetService(); + Configuration config = new Configuration(context.HostingEnvironment.ApplicationName, loggerFactory); + + var tracer = config.GetTracer(); + GlobalTracer.Register(tracer); + return tracer; + }); + + services + .AddOcelot() + .AddOpenTracing(); + }) + .ConfigureLogging(logging => + { + logging.AddConsole(); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + }) + .Build() + .Run(); + } + } +} diff --git a/samples/OcelotOpenTracing/appsettings.Development.json b/samples/OcelotOpenTracing/appsettings.Development.json new file mode 100644 index 000000000..8983e0fc1 --- /dev/null +++ b/samples/OcelotOpenTracing/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/samples/OcelotOpenTracing/appsettings.json b/samples/OcelotOpenTracing/appsettings.json new file mode 100644 index 000000000..d9d9a9bff --- /dev/null +++ b/samples/OcelotOpenTracing/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/OcelotOpenTracing/ocelot.json b/samples/OcelotOpenTracing/ocelot.json new file mode 100644 index 000000000..a5a670321 --- /dev/null +++ b/samples/OcelotOpenTracing/ocelot.json @@ -0,0 +1,24 @@ +{ + "ReRoutes": [ + { + "HttpHandlerOptions": { + "UseTracing": true + }, + "DownstreamPathTemplate": "/todos/{id}", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 443 + } + ], + "UpstreamPathTemplate": "/posts/{id}", + "UpstreamHttpMethod": [ + "Get" + ] + } + ], + "GlobalConfiguration": { + "BaseUrl": "https://localhost:5000" + } +} diff --git a/src/Ocelot.Tracing.OpenTracing/Ocelot.Tracing.OpenTracing.csproj b/src/Ocelot.Tracing.OpenTracing/Ocelot.Tracing.OpenTracing.csproj new file mode 100644 index 000000000..9b6d5abae --- /dev/null +++ b/src/Ocelot.Tracing.OpenTracing/Ocelot.Tracing.OpenTracing.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.1 + 0.0.0-dev + Kjell-Åke Gafvelin + This package provides OpenTracing support to Ocelot. + https://github.com/ThreeMammals/Ocelot + API Gateway;.NET core; OpenTracing + true + + + + + + + + + + + diff --git a/src/Ocelot.Tracing.OpenTracing/OcelotBuilderExtensions.cs b/src/Ocelot.Tracing.OpenTracing/OcelotBuilderExtensions.cs new file mode 100644 index 000000000..59d789758 --- /dev/null +++ b/src/Ocelot.Tracing.OpenTracing/OcelotBuilderExtensions.cs @@ -0,0 +1,15 @@ +namespace Ocelot.Tracing.OpenTracing +{ + using Microsoft.Extensions.DependencyInjection.Extensions; + using Ocelot.DependencyInjection; + using Ocelot.Logging; + + public static class OcelotBuilderExtensions + { + public static IOcelotBuilder AddOpenTracing(this IOcelotBuilder builder) + { + builder.Services.TryAddSingleton(); + return builder; + } + } +} diff --git a/src/Ocelot.Tracing.OpenTracing/OpenTracingTracer.cs b/src/Ocelot.Tracing.OpenTracing/OpenTracingTracer.cs new file mode 100644 index 000000000..744e6ddd3 --- /dev/null +++ b/src/Ocelot.Tracing.OpenTracing/OpenTracingTracer.cs @@ -0,0 +1,75 @@ +namespace Ocelot.Tracing.OpenTracing +{ + using global::OpenTracing; + using global::OpenTracing.Propagation; + using global::OpenTracing.Tag; + using Microsoft.AspNetCore.Http; + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + + class OpenTracingTracer : Logging.ITracer + { + private readonly ITracer _tracer; + + public OpenTracingTracer(ITracer tracer) + { + _tracer = tracer ?? throw new ArgumentNullException(nameof(tracer)); + } + + public void Event(HttpContext httpContext, string @event) + { + } + + public async Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken, + Action addTraceIdToRepo, + Func> baseSendAsync) + { + using (IScope scope = _tracer.BuildSpan(request.RequestUri.AbsoluteUri).StartActive(finishSpanOnDispose: true)) + { + var span = scope.Span; + + span.SetTag(Tags.SpanKind, Tags.SpanKindClient) + .SetTag(Tags.HttpMethod, request.Method.Method) + .SetTag(Tags.HttpUrl, request.RequestUri.OriginalString); + + addTraceIdToRepo(span.Context.SpanId); + + var headers = new Dictionary(); + + _tracer.Inject(span.Context, BuiltinFormats.HttpHeaders, new TextMapInjectAdapter(headers)); + + foreach (var item in headers) + { + request.Headers.Add(item.Key, item.Value); + } + + try + { + var response = await baseSendAsync(request, cancellationToken); + + span.SetTag(Tags.HttpStatus, (int)response.StatusCode); + + return response; + } + catch (HttpRequestException ex) + { + Tags.Error.Set(scope.Span, true); + + span.Log(new Dictionary(3) + { + { LogFields.Event, Tags.Error.Key }, + { LogFields.ErrorKind, ex.GetType().Name }, + { LogFields.ErrorObject, ex } + }); + throw; + } + } + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj index 5adb82a6d..4ddf06a3e 100644 --- a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj @@ -1,75 +1,77 @@ - - - 0.0.0-dev - netcoreapp3.1 - Ocelot.AcceptanceTests - Exe - Ocelot.AcceptanceTests - true - osx.10.11-x64;osx.10.12-x64;win7-x64;win10-x64 - false - false - false - ..\..\codeanalysis.ruleset - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - - - - - - - - - - - - - - - - - - all - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - + + + 0.0.0-dev + netcoreapp3.1 + Ocelot.AcceptanceTests + Exe + Ocelot.AcceptanceTests + true + osx.10.11-x64;osx.10.12-x64;win7-x64;win10-x64 + false + false + false + ..\..\codeanalysis.ruleset + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/OpenTracingTests.cs b/test/Ocelot.AcceptanceTests/OpenTracingTests.cs new file mode 100644 index 000000000..d93f9c1b4 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/OpenTracingTests.cs @@ -0,0 +1,516 @@ +namespace Ocelot.AcceptanceTests +{ + using Butterfly.Client.AspNetCore; + using Ocelot.Configuration.File; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Http; + using OpenTracing; + using OpenTracing.Propagation; + using OpenTracing.Tag; + using Rafty.Infrastructure; + using Shouldly; + using System; + using System.Collections.Generic; + using System.IO; + using System.Net; + using TestStack.BDDfy; + using Xunit; + using Xunit.Abstractions; + + public class OpenTracingTests : IDisposable + { + private IWebHost _serviceOneBuilder; + private IWebHost _serviceTwoBuilder; + private IWebHost _fakeOpenTracing; + private readonly Steps _steps; + private string _downstreamPathOne; + private string _downstreamPathTwo; + private readonly ITestOutputHelper _output; + + public OpenTracingTests(ITestOutputHelper output) + { + _output = output; + _steps = new Steps(); + } + + [Fact] + public void should_forward_tracing_information_from_ocelot_and_downstream_services() + { + int port1 = RandomPortFinder.GetRandomPort(); + int port2 = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration() + { + Routes = new List() + { + new FileRoute() + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = port1, + } + }, + UpstreamPathTemplate = "/api001/values", + UpstreamHttpMethod = new List { "Get" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseTracing = true + } + }, + new FileRoute() + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort() + { + Host = "localhost", + Port = port2, + } + }, + UpstreamPathTemplate = "/api002/values", + UpstreamHttpMethod = new List { "Get" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseTracing = true + } + } + } + }; + + var tracingPort = RandomPortFinder.GetRandomPort(); + var tracingUrl = $"http://localhost:{tracingPort}"; + + var fakeTracer = new FakeTracer(); + + this.Given(_ => GivenFakeOpenTracing(tracingUrl)) + .And(_ => GivenServiceOneIsRunning($"http://localhost:{port1}", "/api/values", 200, "Hello from Laura", tracingUrl)) + .And(_ => GivenServiceTwoIsRunning($"http://localhost:{port2}", "/api/values", 200, "Hello from Tom", tracingUrl)) + .And(_ => _steps.GivenThereIsAConfiguration(configuration)) + .And(_ => _steps.GivenOcelotIsRunningUsingOpenTracing(fakeTracer)) + .When(_ => _steps.WhenIGetUrlOnTheApiGateway("/api001/values")) + .Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .When(_ => _steps.WhenIGetUrlOnTheApiGateway("/api002/values")) + .Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) + .And(_ => ThenTheTracerIsCalled(fakeTracer)) + .BDDfy(); + } + + [Fact] + public void should_return_tracing_header() + { + int port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new FileRoute + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = port, + } + }, + UpstreamPathTemplate = "/api001/values", + UpstreamHttpMethod = new List { "Get" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseTracing = true + }, + DownstreamHeaderTransform = new Dictionary() + { + {"Trace-Id", "{TraceId}"}, + {"Tom", "Laura"} + } + } + } + }; + + var butterflyPort = RandomPortFinder.GetRandomPort(); + + var butterflyUrl = $"http://localhost:{butterflyPort}"; + + var fakeTracer = new FakeTracer(); + + this.Given(x => GivenFakeOpenTracing(butterflyUrl)) + .And(x => GivenServiceOneIsRunning($"http://localhost:{port}", "/api/values", 200, "Hello from Laura", butterflyUrl)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingOpenTracing(fakeTracer)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api001/values")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.ThenTheTraceHeaderIsSet("Trace-Id")) + .And(x => _steps.ThenTheResponseHeaderIs("Tom", "Laura")) + .BDDfy(); + } + + private void ThenTheTracerIsCalled(FakeTracer fakeTracer) + { + var commandOnAllStateMachines = Wait.WaitFor(10000).Until(() => fakeTracer.BuildSpanCalled >= 2); + + _output.WriteLine($"fakeTracer.BuildSpanCalled is {fakeTracer.BuildSpanCalled}"); + + commandOnAllStateMachines.ShouldBeTrue(); + } + + private void GivenServiceOneIsRunning(string baseUrl, string basePath, int statusCode, string responseBody, string butterflyUrl) + { + _serviceOneBuilder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .ConfigureServices(services => + { + services.AddButterfly(option => + { + option.CollectorUrl = butterflyUrl; + option.Service = "Service One"; + option.IgnoredRoutesRegexPatterns = new string[0]; + }); + }) + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + _downstreamPathOne = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPathOne != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + }) + .Build(); + + _serviceOneBuilder.Start(); + } + + private void GivenFakeOpenTracing(string baseUrl) + { + _fakeOpenTracing = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.Run(async context => + { + await context.Response.WriteAsync("OK..."); + }); + }) + .Build(); + + _fakeOpenTracing.Start(); + } + + private void GivenServiceTwoIsRunning(string baseUrl, string basePath, int statusCode, string responseBody, string butterflyUrl) + { + _serviceTwoBuilder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .ConfigureServices(services => + { + services.AddButterfly(option => + { + option.CollectorUrl = butterflyUrl; + option.Service = "Service Two"; + option.IgnoredRoutesRegexPatterns = new string[0]; + }); + }) + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + _downstreamPathTwo = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if (_downstreamPathTwo != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + }) + .Build(); + + _serviceTwoBuilder.Start(); + } + + public void Dispose() + { + _serviceOneBuilder?.Dispose(); + _serviceTwoBuilder?.Dispose(); + _fakeOpenTracing?.Dispose(); + _steps.Dispose(); + } + } + + internal class FakeTracer : ITracer + { + public IScopeManager ScopeManager => throw new NotImplementedException(); + + public ISpan ActiveSpan => throw new NotImplementedException(); + + public ISpanBuilder BuildSpan(string operationName) + { + this.BuildSpanCalled++; + + return new FakeSpanBuilder(); + } + + public int BuildSpanCalled { get; set; } + + public ISpanContext Extract(IFormat format, TCarrier carrier) + { + this.ExtractCalled++; + + return null; + } + + public int ExtractCalled { get; set; } + + public void Inject(ISpanContext spanContext, IFormat format, TCarrier carrier) + { + this.InjectCalled++; + } + + public int InjectCalled { get; set; } + } + + internal class FakeSpanBuilder : ISpanBuilder + { + public ISpanBuilder AddReference(string referenceType, ISpanContext referencedContext) + { + throw new NotImplementedException(); + } + + public ISpanBuilder AsChildOf(ISpanContext parent) + { + throw new NotImplementedException(); + } + + public ISpanBuilder AsChildOf(ISpan parent) + { + throw new NotImplementedException(); + } + + public ISpanBuilder IgnoreActiveSpan() + { + throw new NotImplementedException(); + } + + public ISpan Start() + { + throw new NotImplementedException(); + } + + public IScope StartActive() + { + throw new NotImplementedException(); + } + + public IScope StartActive(bool finishSpanOnDispose) + { + return new FakeScope(finishSpanOnDispose); + } + + public ISpanBuilder WithStartTimestamp(DateTimeOffset timestamp) + { + throw new NotImplementedException(); + } + + public ISpanBuilder WithTag(string key, string value) + { + throw new NotImplementedException(); + } + + public ISpanBuilder WithTag(string key, bool value) + { + throw new NotImplementedException(); + } + + public ISpanBuilder WithTag(string key, int value) + { + throw new NotImplementedException(); + } + + public ISpanBuilder WithTag(string key, double value) + { + throw new NotImplementedException(); + } + + public ISpanBuilder WithTag(BooleanTag tag, bool value) + { + throw new NotImplementedException(); + } + + public ISpanBuilder WithTag(IntOrStringTag tag, string value) + { + throw new NotImplementedException(); + } + + public ISpanBuilder WithTag(IntTag tag, int value) + { + throw new NotImplementedException(); + } + + public ISpanBuilder WithTag(StringTag tag, string value) + { + throw new NotImplementedException(); + } + } + + internal class FakeScope : IScope + { + private readonly bool finishSpanOnDispose; + + public FakeScope(bool finishSpanOnDispose) + { + this.finishSpanOnDispose = finishSpanOnDispose; + } + + public ISpan Span { get; } = new FakeSpan(); + + public void Dispose() + { + if (this.finishSpanOnDispose) + { + this.Span.Finish(); + } + } + } + + internal class FakeSpan : ISpan + { + public ISpanContext Context => new FakeSpanContext(); + + public void Finish() + { + } + + public void Finish(DateTimeOffset finishTimestamp) + { + throw new NotImplementedException(); + } + + public string GetBaggageItem(string key) + { + throw new NotImplementedException(); + } + + public ISpan Log(IEnumerable> fields) + { + return this; + } + + public ISpan Log(DateTimeOffset timestamp, IEnumerable> fields) + { + throw new NotImplementedException(); + } + + public ISpan Log(string @event) + { + throw new NotImplementedException(); + } + + public ISpan Log(DateTimeOffset timestamp, string @event) + { + throw new NotImplementedException(); + } + + public ISpan SetBaggageItem(string key, string value) + { + throw new NotImplementedException(); + } + + public ISpan SetOperationName(string operationName) + { + throw new NotImplementedException(); + } + + public ISpan SetTag(string key, string value) + { + return this; + } + + public ISpan SetTag(string key, bool value) + { + return this; + } + + public ISpan SetTag(string key, int value) + { + return this; + } + + public ISpan SetTag(string key, double value) + { + return this; + } + + public ISpan SetTag(BooleanTag tag, bool value) + { + return this; + } + + public ISpan SetTag(IntOrStringTag tag, string value) + { + return this; + } + + public ISpan SetTag(IntTag tag, int value) + { + return this; + } + + public ISpan SetTag(StringTag tag, string value) + { + return this; + } + } + + internal class FakeSpanContext : ISpanContext + { + public static string FakeTraceId = "FakeTraceId"; + + public static string FakeSpanId = "FakeSpanId"; + + public string TraceId => FakeTraceId; + + public string SpanId => FakeSpanId; + + public IEnumerable> GetBaggageItems() + { + throw new NotImplementedException(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index de2556b28..453303117 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -46,6 +46,7 @@ namespace Ocelot.AcceptanceTests using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; using CookieHeaderValue = Microsoft.Net.Http.Headers.CookieHeaderValue; using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue; + using Ocelot.Tracing.OpenTracing; public class Steps : IDisposable { @@ -1214,6 +1215,41 @@ public void GivenOcelotIsRunningWithLogger() _ocelotClient = _ocelotServer.CreateClient(); } + internal void GivenOcelotIsRunningUsingOpenTracing(OpenTracing.ITracer fakeTracer) + { + _webHostBuilder = new WebHostBuilder(); + + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); + config.AddJsonFile("ocelot.json", optional: true, reloadOnChange: false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot() + .AddOpenTracing(); + + s.AddSingleton(fakeTracer); + }) + .Configure(app => + { + app.Use(async (_, next) => + { + await next.Invoke(); + }); + app.UseOcelot().Wait(); + }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + public void ThenWarningShouldBeLogged() { MockLoggerFactory loggerFactory = (MockLoggerFactory)_ocelotServer.Host.Services.GetService();