From 56fe7b5ba9bdb402ccd07d90963aab28571ff17a Mon Sep 17 00:00:00 2001 From: TomPallister Date: Mon, 25 May 2020 16:59:49 +0100 Subject: [PATCH 1/2] wip --- .../OcelotOpenTracing.csproj | 30 + samples/OcelotOpenTracing/Program.cs | 67 +++ .../appsettings.Development.json | 9 + samples/OcelotOpenTracing/appsettings.json | 10 + samples/OcelotOpenTracing/ocelot.json | 24 + .../Ocelot.Tracing.OpenTracing.csproj | 21 + .../OcelotBuilderExtensions.cs | 17 + .../OpenTracingTracer.cs | 71 +++ .../OpenTracingTests.cs | 511 ++++++++++++++++++ 9 files changed, 760 insertions(+) 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/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..5a2984c4a --- /dev/null +++ b/samples/OcelotOpenTracing/Program.cs @@ -0,0 +1,67 @@ +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; + +namespace OcelotOpenTracing +{ + 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 + .AddOcelot() + .AddOpenTracing(); + + services.AddSingleton(sp => + { + var loggerFactory = sp.GetService(); + Configuration config = new Configuration(context.HostingEnvironment.ApplicationName, loggerFactory); + + var tracer = config.GetTracer(); + GlobalTracer.Register(tracer); + return tracer; + }); + + }) + .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..c2a3e9fd6 --- /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..d6243d5da --- /dev/null +++ b/src/Ocelot.Tracing.OpenTracing/OcelotBuilderExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using Ocelot.DependencyInjection; +using Ocelot.Logging; +using System; + +namespace Ocelot.Tracing.OpenTracing +{ + public static class OcelotBuilderExtensions + { + public static IOcelotBuilder AddOpenTracing(this IOcelotBuilder builder) + { + builder.Services.AddSingleton(); + + return builder; + } + } +} diff --git a/src/Ocelot.Tracing.OpenTracing/OpenTracingTracer.cs b/src/Ocelot.Tracing.OpenTracing/OpenTracingTracer.cs new file mode 100644 index 000000000..875cd4310 --- /dev/null +++ b/src/Ocelot.Tracing.OpenTracing/OpenTracingTracer.cs @@ -0,0 +1,71 @@ +using Microsoft.AspNetCore.Http; +using OpenTracing; +using OpenTracing.Propagation; +using OpenTracing.Tag; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Ocelot.Tracing.OpenTracing +{ + class OpenTracingTracer : Logging.ITracer + { + private readonly ITracer tracer; + + public OpenTracingTracer(ITracer tracer) + { + this.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 = this.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(); + + this.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/OpenTracingTests.cs b/test/Ocelot.AcceptanceTests/OpenTracingTests.cs new file mode 100644 index 000000000..f4dbaff4f --- /dev/null +++ b/test/Ocelot.AcceptanceTests/OpenTracingTests.cs @@ -0,0 +1,511 @@ +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() + { + ReRoutes = new List() + { + new FileReRoute() + { + 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 FileReRoute() + { + 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")) + .BDDfy(); + + var commandOnAllStateMachines = Wait.WaitFor(10000).Until(() => fakeTracer.BuildSpanCalled >= 2); + + _output.WriteLine($"fakeTracer.BuildSpanCalled is {fakeTracer.BuildSpanCalled}"); + + commandOnAllStateMachines.ShouldBeTrue(); + } + + [Fact] + public void should_return_tracing_header() + { + int port = RandomPortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + 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 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(); + } + } +} From 21b9d73c7f7a1affe746bf54f643fb42038e2392 Mon Sep 17 00:00:00 2001 From: TomPallister Date: Mon, 25 May 2020 18:39:00 +0100 Subject: [PATCH 2/2] tweaks --- Ocelot.sln | 17 ++ docs/features/tracing.rst | 37 ++++- samples/OcelotOpenTracing/Program.cs | 36 ++--- .../Ocelot.Tracing.OpenTracing.csproj | 4 +- .../OcelotBuilderExtensions.cs | 14 +- .../OpenTracingTracer.cs | 38 +++-- .../Ocelot.AcceptanceTests.csproj | 150 +++++++++--------- .../OpenTracingTests.cs | 27 ++-- test/Ocelot.AcceptanceTests/Steps.cs | 36 +++++ 9 files changed, 226 insertions(+), 133 deletions(-) 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/Program.cs b/samples/OcelotOpenTracing/Program.cs index 5a2984c4a..e9527930f 100644 --- a/samples/OcelotOpenTracing/Program.cs +++ b/samples/OcelotOpenTracing/Program.cs @@ -1,23 +1,22 @@ -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; - -namespace OcelotOpenTracing +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 => { @@ -36,10 +35,6 @@ private static void Main(string[] args) }) .ConfigureServices((context, services) => { - services - .AddOcelot() - .AddOpenTracing(); - services.AddSingleton(sp => { var loggerFactory = sp.GetService(); @@ -50,6 +45,9 @@ private static void Main(string[] args) return tracer; }); + services + .AddOcelot() + .AddOpenTracing(); }) .ConfigureLogging(logging => { diff --git a/src/Ocelot.Tracing.OpenTracing/Ocelot.Tracing.OpenTracing.csproj b/src/Ocelot.Tracing.OpenTracing/Ocelot.Tracing.OpenTracing.csproj index c2a3e9fd6..9b6d5abae 100644 --- a/src/Ocelot.Tracing.OpenTracing/Ocelot.Tracing.OpenTracing.csproj +++ b/src/Ocelot.Tracing.OpenTracing/Ocelot.Tracing.OpenTracing.csproj @@ -10,8 +10,8 @@ true - - + + diff --git a/src/Ocelot.Tracing.OpenTracing/OcelotBuilderExtensions.cs b/src/Ocelot.Tracing.OpenTracing/OcelotBuilderExtensions.cs index d6243d5da..59d789758 100644 --- a/src/Ocelot.Tracing.OpenTracing/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Tracing.OpenTracing/OcelotBuilderExtensions.cs @@ -1,16 +1,14 @@ -using Microsoft.Extensions.DependencyInjection; -using Ocelot.DependencyInjection; -using Ocelot.Logging; -using System; - -namespace Ocelot.Tracing.OpenTracing +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.AddSingleton(); - + builder.Services.TryAddSingleton(); return builder; } } diff --git a/src/Ocelot.Tracing.OpenTracing/OpenTracingTracer.cs b/src/Ocelot.Tracing.OpenTracing/OpenTracingTracer.cs index 875cd4310..744e6ddd3 100644 --- a/src/Ocelot.Tracing.OpenTracing/OpenTracingTracer.cs +++ b/src/Ocelot.Tracing.OpenTracing/OpenTracingTracer.cs @@ -1,32 +1,36 @@ -using Microsoft.AspNetCore.Http; -using OpenTracing; -using OpenTracing.Propagation; -using OpenTracing.Tag; -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace Ocelot.Tracing.OpenTracing +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; + private readonly ITracer _tracer; public OpenTracingTracer(ITracer tracer) { - this.tracer = tracer ?? throw new ArgumentNullException(nameof(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) + public async Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken, + Action addTraceIdToRepo, + Func> baseSendAsync) { - using (IScope scope = this.tracer.BuildSpan(request.RequestUri.AbsoluteUri).StartActive(finishSpanOnDispose: true)) + using (IScope scope = _tracer.BuildSpan(request.RequestUri.AbsoluteUri).StartActive(finishSpanOnDispose: true)) { var span = scope.Span; @@ -38,7 +42,7 @@ public async Task SendAsync(HttpRequestMessage request, Can var headers = new Dictionary(); - this.tracer.Inject(span.Context, BuiltinFormats.HttpHeaders, new TextMapInjectAdapter(headers)); + _tracer.Inject(span.Context, BuiltinFormats.HttpHeaders, new TextMapInjectAdapter(headers)); foreach (var item in headers) { 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 index f4dbaff4f..d93f9c1b4 100644 --- a/test/Ocelot.AcceptanceTests/OpenTracingTests.cs +++ b/test/Ocelot.AcceptanceTests/OpenTracingTests.cs @@ -41,9 +41,9 @@ public void should_forward_tracing_information_from_ocelot_and_downstream_servic int port2 = RandomPortFinder.GetRandomPort(); var configuration = new FileConfiguration() { - ReRoutes = new List() + Routes = new List() { - new FileReRoute() + new FileRoute() { DownstreamPathTemplate = "/api/values", DownstreamScheme = "http", @@ -62,7 +62,7 @@ public void should_forward_tracing_information_from_ocelot_and_downstream_servic UseTracing = true } }, - new FileReRoute() + new FileRoute() { DownstreamPathTemplate = "/api/values", DownstreamScheme = "http", @@ -100,13 +100,8 @@ public void should_forward_tracing_information_from_ocelot_and_downstream_servic .When(_ => _steps.WhenIGetUrlOnTheApiGateway("/api002/values")) .Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) + .And(_ => ThenTheTracerIsCalled(fakeTracer)) .BDDfy(); - - var commandOnAllStateMachines = Wait.WaitFor(10000).Until(() => fakeTracer.BuildSpanCalled >= 2); - - _output.WriteLine($"fakeTracer.BuildSpanCalled is {fakeTracer.BuildSpanCalled}"); - - commandOnAllStateMachines.ShouldBeTrue(); } [Fact] @@ -115,9 +110,9 @@ public void should_return_tracing_header() int port = RandomPortFinder.GetRandomPort(); var configuration = new FileConfiguration { - ReRoutes = new List + Routes = new List { - new FileReRoute + new FileRoute { DownstreamPathTemplate = "/api/values", DownstreamScheme = "http", @@ -145,6 +140,7 @@ public void should_return_tracing_header() }; var butterflyPort = RandomPortFinder.GetRandomPort(); + var butterflyUrl = $"http://localhost:{butterflyPort}"; var fakeTracer = new FakeTracer(); @@ -161,6 +157,15 @@ public void should_return_tracing_header() .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() 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();