diff --git a/samples/OcelotOpenTracing/OcelotOpenTracing.csproj b/samples/OcelotOpenTracing/OcelotOpenTracing.csproj
new file mode 100644
index 00000000..295fc461
--- /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 00000000..5a2984c4
--- /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 00000000..8983e0fc
--- /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 00000000..d9d9a9bf
--- /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 00000000..a5a67032
--- /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 00000000..c2a3e9fd
--- /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 00000000..d6243d5d
--- /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 00000000..875cd431
--- /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 00000000..f4dbaff4
--- /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();
+ }
+ }
+}