broke out butterfly into seperate project (#521)

* broke out butterfly into seperate project

* nearly did it...

* updated docs as I have broken the butterfly code off into a seperate dll
pull/530/head 8.0.6
Tom Pallister 7 years ago committed by GitHub
parent 24f8a18579
commit 37fb32b7f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,19 +1,29 @@
Tracing
=======
Ocelot providers tracing functionality from the excellent `Butterfly <https://github.com/liuhaoyang/butterfly>`_ project.
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.
Butterfly
^^^^^^^^^
Ocelot providers tracing functionality from the excellent `Butterfly <https://github.com/liuhaoyang/butterfly>`_ project. The code for the Ocelot integration
can be found `here <https://github.com/ThreeMammals/Ocelot.Tracing.Butterfly>`_.
In order to use the tracing please read the Butterfly documentation.
In ocelot you need to do the following if you wish to trace a ReRoute.
``Install-Package Ocelot.Tracing.Butterfly``
In your ConfigureServices method
.. code-block:: csharp
services
.AddOcelot()
.AddOpenTracing(option =>
// this comes from Ocelot.Tracing.Butterfly package
.AddButterfly(option =>
{
//this is the url that the butterfly collector server is running on...
option.CollectorUrl = "http://localhost:9618";

@ -1,18 +1,17 @@
namespace Ocelot.Configuration.Creator
{
using System;
using Butterfly.Client.Tracing;
using Logging;
using Microsoft.Extensions.DependencyInjection;
using Ocelot.Configuration.File;
using Ocelot.Requester;
public class HttpHandlerOptionsCreator : IHttpHandlerOptionsCreator
{
private readonly IServiceTracer _tracer;
private readonly ITracer _tracer;
public HttpHandlerOptionsCreator(IServiceProvider services)
{
_tracer = services.GetService<IServiceTracer>();
_tracer = services.GetService<ITracer>();
}
public HttpHandlerOptions Create(FileHttpHandlerOptions options)

@ -1,4 +1,3 @@
using Butterfly.Client.AspNetCore;
using CacheManager.Core;
using System;
using System.Net.Http;
@ -17,8 +16,6 @@ namespace Ocelot.DependencyInjection
IOcelotBuilder AddCacheManager(Action<ConfigurationBuilderCachePart> settings);
IOcelotBuilder AddOpenTracing(Action<ButterflyOptions> settings);
IOcelotAdministrationBuilder AddAdministration(string path, string secret);
IOcelotAdministrationBuilder AddAdministration(string path, Action<IdentityServerAuthenticationOptions> configOptions);

@ -40,10 +40,8 @@ namespace Ocelot.DependencyInjection
using Ocelot.Configuration;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Net.Http;
using Butterfly.Client.AspNetCore;
using Ocelot.Infrastructure;
using Ocelot.Infrastructure.Consul;
using Butterfly.Client.Tracing;
using Ocelot.Middleware.Multiplexer;
using ServiceDiscovery.Providers;
using Steeltoe.Common.Discovery;
@ -228,12 +226,6 @@ namespace Ocelot.DependencyInjection
return this;
}
public IOcelotBuilder AddOpenTracing(Action<ButterflyOptions> settings)
{
_services.AddButterfly(settings);
return this;
}
public IOcelotBuilder AddStoreOcelotConfigurationInConsul()
{
_services.AddHostedService<FileConfigurationPoller>();

@ -2,26 +2,20 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DiagnosticAdapter;
using Microsoft.Extensions.DependencyInjection;
using Butterfly.Client.AspNetCore;
using Butterfly.OpenTracing;
using Ocelot.Middleware;
using Butterfly.Client.Tracing;
using System.Linq;
using System.Collections.Generic;
using Ocelot.Infrastructure.Extensions;
using Ocelot.Requester;
namespace Ocelot.Logging
{
public class OcelotDiagnosticListener
{
private readonly IServiceTracer _tracer;
private readonly IOcelotLogger _logger;
private readonly ITracer _tracer;
public OcelotDiagnosticListener(IOcelotLoggerFactory factory, IServiceProvider services)
public OcelotDiagnosticListener(IOcelotLoggerFactory factory, IServiceProvider serviceProvider)
{
_tracer = services.GetService<IServiceTracer>();
_logger = factory.CreateLogger<OcelotDiagnosticListener>();
_tracer = serviceProvider.GetService<ITracer>();
}
[DiagnosticName("Ocelot.MiddlewareException")]
@ -67,29 +61,7 @@ namespace Ocelot.Logging
private void Event(HttpContext httpContext, string @event)
{
// todo - if the user isnt using tracing the code gets here and will blow up on
// _tracer.Tracer.TryExtract..
if(_tracer == null)
{
return;
}
var span = httpContext.GetSpan();
if(span == null)
{
var spanBuilder = new SpanBuilder($"server {httpContext.Request.Method} {httpContext.Request.Path}");
if (_tracer.Tracer.TryExtract(out var spanContext, httpContext.Request.Headers, (c, k) => c[k].GetValue(),
c => c.Select(x => new KeyValuePair<string, string>(x.Key, x.Value.GetValue())).GetEnumerator()))
{
spanBuilder.AsChildOf(spanContext);
}
span = _tracer.Start(spanBuilder);
httpContext.SetSpan(span);
}
span?.Log(LogField.CreateNew().Event(@event));
_tracer?.Event(httpContext, @event);
}
}
}

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RuntimeFrameworkVersion>2.0.0</RuntimeFrameworkVersion>
@ -25,10 +25,6 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Butterfly.Client" Version="0.0.8" />
<PackageReference Include="Butterfly.Client.AspNetCore" Version="0.0.8">
<NoWarn>NU1701</NoWarn>
</PackageReference>
<PackageReference Include="FluentValidation" Version="7.6.104" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="2.6.0" />
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.1" />

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using Butterfly.Client.Tracing;
using Microsoft.Extensions.DependencyInjection;
using Ocelot.Configuration;
using Ocelot.Logging;

@ -1,21 +1,19 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Butterfly.Client.Tracing;
using Butterfly.OpenTracing;
using Ocelot.Infrastructure.RequestData;
namespace Ocelot.Requester
namespace Ocelot.Requester
{
using Logging;
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Ocelot.Infrastructure.RequestData;
public class OcelotHttpTracingHandler : DelegatingHandler, ITracingHandler
{
private readonly IServiceTracer _tracer;
private readonly ITracer _tracer;
private readonly IRequestScopedDataRepository _repo;
private const string PrefixSpanId = "ot-spanId";
public OcelotHttpTracingHandler(
IServiceTracer tracer,
ITracer tracer,
IRequestScopedDataRepository repo,
HttpMessageHandler httpMessageHandler = null)
{
@ -28,46 +26,8 @@ namespace Ocelot.Requester
HttpRequestMessage request,
CancellationToken cancellationToken)
{
return _tracer.ChildTraceAsync($"httpclient {request.Method}", DateTimeOffset.UtcNow, span => TracingSendAsync(span, request, cancellationToken));
}
protected virtual async Task<HttpResponseMessage> TracingSendAsync(
ISpan span,
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (request.Headers.Contains(PrefixSpanId))
{
request.Headers.Remove(PrefixSpanId);
request.Headers.TryAddWithoutValidation(PrefixSpanId, span.SpanContext.SpanId);
}
_repo.Add("TraceId", span.SpanContext.TraceId);
span.Tags.Client().Component("HttpClient")
.HttpMethod(request.Method.Method)
.HttpUrl(request.RequestUri.OriginalString)
.HttpHost(request.RequestUri.Host)
.HttpPath(request.RequestUri.PathAndQuery)
.PeerAddress(request.RequestUri.OriginalString)
.PeerHostName(request.RequestUri.Host)
.PeerPort(request.RequestUri.Port);
_tracer.Tracer.Inject(span.SpanContext, request.Headers, (c, k, v) =>
{
if (!c.Contains(k))
{
c.Add(k, v);
}
});
span.Log(LogField.CreateNew().ClientSend());
var responseMessage = await base.SendAsync(request, cancellationToken);
span.Log(LogField.CreateNew().ClientReceive());
return responseMessage;
return _tracer.SendAsync(request, cancellationToken, x => _repo.Add("TraceId", x), (r,c) => base.SendAsync(r, c));
}
}
}

@ -1,13 +1,13 @@
namespace Ocelot.Requester
{
using System;
using Butterfly.Client.Tracing;
using Logging;
using Ocelot.Infrastructure.RequestData;
using Microsoft.Extensions.DependencyInjection;
public class TracingHandlerFactory : ITracingHandlerFactory
{
private readonly IServiceTracer _tracer;
private readonly ITracer _tracer;
private readonly IRequestScopedDataRepository _repo;
public TracingHandlerFactory(
@ -15,7 +15,7 @@ namespace Ocelot.Requester
IRequestScopedDataRepository repo)
{
_repo = repo;
_tracer = services.GetService<IServiceTracer>();
_tracer = services.GetService<ITracer>();
}
public ITracingHandler Get()

@ -1,284 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Ocelot.Configuration.File;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
using Butterfly.Client.AspNetCore;
using static Rafty.Infrastructure.Wait;
namespace Ocelot.AcceptanceTests
{
using Xunit.Abstractions;
public class ButterflyTracingTests : IDisposable
{
private IWebHost _serviceOneBuilder;
private IWebHost _serviceTwoBuilder;
private IWebHost _fakeButterfly;
private readonly Steps _steps;
private string _downstreamPathOne;
private string _downstreamPathTwo;
private int _butterflyCalled;
private readonly ITestOutputHelper _output;
public ButterflyTracingTests(ITestOutputHelper output)
{
_output = output;
_steps = new Steps();
}
[Fact]
public void should_forward_tracing_information_from_ocelot_and_downstream_services()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/api/values",
DownstreamScheme = "http",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 51887,
}
},
UpstreamPathTemplate = "/api001/values",
UpstreamHttpMethod = new List<string> { "Get" },
HttpHandlerOptions = new FileHttpHandlerOptions
{
UseTracing = true
},
QoSOptions = new FileQoSOptions
{
ExceptionsAllowedBeforeBreaking = 3,
DurationOfBreak = 10,
TimeoutValue = 5000
}
},
new FileReRoute
{
DownstreamPathTemplate = "/api/values",
DownstreamScheme = "http",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 51388,
}
},
UpstreamPathTemplate = "/api002/values",
UpstreamHttpMethod = new List<string> { "Get" },
HttpHandlerOptions = new FileHttpHandlerOptions
{
UseTracing = true
},
QoSOptions = new FileQoSOptions
{
ExceptionsAllowedBeforeBreaking = 3,
DurationOfBreak = 10,
TimeoutValue = 5000
}
}
}
};
var butterflyUrl = "http://localhost:9618";
this.Given(x => GivenFakeButterfly(butterflyUrl))
.And(x => GivenServiceOneIsRunning("http://localhost:51887", "/api/values", 200, "Hello from Laura", butterflyUrl))
.And(x => GivenServiceTwoIsRunning("http://localhost:51388", "/api/values", 200, "Hello from Tom", butterflyUrl))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunningUsingButterfly(butterflyUrl))
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/api001/values"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/api002/values"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom"))
.BDDfy();
var commandOnAllStateMachines = WaitFor(10000).Until(() => _butterflyCalled >= 4);
_output.WriteLine($"_butterflyCalled is {_butterflyCalled}");
commandOnAllStateMachines.ShouldBeTrue();
}
[Fact]
public void should_return_tracing_header()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/api/values",
DownstreamScheme = "http",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 51387,
}
},
UpstreamPathTemplate = "/api001/values",
UpstreamHttpMethod = new List<string> { "Get" },
HttpHandlerOptions = new FileHttpHandlerOptions
{
UseTracing = true
},
QoSOptions = new FileQoSOptions
{
ExceptionsAllowedBeforeBreaking = 3,
DurationOfBreak = 10,
TimeoutValue = 5000
},
DownstreamHeaderTransform = new Dictionary<string, string>()
{
{"Trace-Id", "{TraceId}"},
{"Tom", "Laura"}
}
}
}
};
var butterflyUrl = "http://localhost:9618";
this.Given(x => GivenFakeButterfly(butterflyUrl))
.And(x => GivenServiceOneIsRunning("http://localhost:51387", "/api/values", 200, "Hello from Laura", butterflyUrl))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunningUsingButterfly(butterflyUrl))
.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 GivenFakeButterfly(string baseUrl)
{
_fakeButterfly = new WebHostBuilder()
.UseUrls(baseUrl)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.Configure(app =>
{
app.Run(async context =>
{
_butterflyCalled++;
await context.Response.WriteAsync("OK...");
});
})
.Build();
_fakeButterfly.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();
}
internal void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPathOne, string expectedDownstreamPath)
{
_downstreamPathOne.ShouldBe(expectedDownstreamPathOne);
_downstreamPathTwo.ShouldBe(expectedDownstreamPath);
}
public void Dispose()
{
_serviceOneBuilder?.Dispose();
_serviceTwoBuilder?.Dispose();
_fakeButterfly?.Dispose();
_steps.Dispose();
}
}
}

@ -31,6 +31,7 @@ using static Ocelot.Infrastructure.Wait;
namespace Ocelot.AcceptanceTests
{
using Butterfly;
using Configuration.Repository;
using Microsoft.Net.Http.Headers;
using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue;
@ -146,44 +147,6 @@ namespace Ocelot.AcceptanceTests
_ocelotClient = _ocelotServer.CreateClient();
}
internal void GivenOcelotIsRunningUsingButterfly(string butterflyUrl)
{
_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(option =>
{
//this is the url that the butterfly collector server is running on...
option.CollectorUrl = butterflyUrl;
option.Service = "Ocelot";
});
})
.Configure(app =>
{
app.Use(async (context, next) =>
{
await next.Invoke();
});
app.UseOcelot().Wait();
});
_ocelotServer = new TestServer(_webHostBuilder);
_ocelotClient = _ocelotServer.CreateClient();
}
internal void GivenIWait(int wait)
{
Thread.Sleep(wait);

@ -1,6 +1,4 @@
using System;
using Butterfly.Client.Tracing;
using Butterfly.OpenTracing;
using Microsoft.Extensions.DependencyInjection;
using Ocelot.Configuration;
using Ocelot.Configuration.Creator;
@ -12,6 +10,12 @@ using Xunit;
namespace Ocelot.UnitTests.Configuration
{
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Ocelot.Logging;
public class HttpHandlerOptionsCreatorTests
{
private IHttpHandlerOptionsCreator _httpHandlerOptionsCreator;
@ -155,23 +159,21 @@ namespace Ocelot.UnitTests.Configuration
private void GivenARealTracer()
{
var tracer = new RealTracer();
_serviceCollection.AddSingleton<IServiceTracer, RealTracer>();
var tracer = new FakeTracer();
_serviceCollection.AddSingleton<ITracer, FakeTracer>();
_serviceProvider = _serviceCollection.BuildServiceProvider();
_httpHandlerOptionsCreator = new HttpHandlerOptionsCreator(_serviceProvider);
}
class RealTracer : IServiceTracer
class FakeTracer : ITracer
{
public ITracer Tracer => throw new NotImplementedException();
public string ServiceName => throw new NotImplementedException();
public string Environment => throw new NotImplementedException();
public string Identity => throw new NotImplementedException();
public void Event(HttpContext httpContext, string @event)
{
throw new NotImplementedException();
}
public ISpan Start(ISpanBuilder spanBuilder)
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken, Action<string> addTraceIdToRepo,
Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> baseSendAsync)
{
throw new NotImplementedException();
}

@ -23,6 +23,8 @@ using Ocelot.Middleware.Multiplexer;
namespace Ocelot.UnitTests.DependencyInjection
{
using Butterfly;
public class OcelotBuilderTests
{
private readonly IServiceCollection _services;
@ -140,15 +142,6 @@ namespace Ocelot.UnitTests.DependencyInjection
.BDDfy();
}
[Fact]
public void should_set_up_tracing()
{
this.Given(x => WhenISetUpOcelotServices())
.When(x => WhenISetUpOpentracing())
.When(x => WhenIAccessOcelotHttpTracingHandler())
.BDDfy();
}
[Fact]
public void should_set_up_without_passing_in_config()
{
@ -387,24 +380,6 @@ namespace Ocelot.UnitTests.DependencyInjection
}
}
private void WhenISetUpOpentracing()
{
try
{
_ocelotBuilder.AddOpenTracing(
option =>
{
option.CollectorUrl = "http://localhost:9618";
option.Service = "Ocelot.ManualTest";
}
);
}
catch (Exception e)
{
_ex = e;
}
}
private void WhenIAccessLoggerFactory()
{
try

@ -1,27 +1,27 @@
using System;
using Butterfly.Client.Tracing;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Ocelot.Infrastructure.RequestData;
using Ocelot.Requester;
using Shouldly;
using Xunit;
namespace Ocelot.UnitTests.Requester
{
using System;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Ocelot.Infrastructure.RequestData;
using Ocelot.Requester;
using Shouldly;
using Xunit;
using Ocelot.Logging;
public class TracingHandlerFactoryTests
{
private TracingHandlerFactory _factory;
private Mock<IServiceTracer> _tracer;
private readonly TracingHandlerFactory _factory;
private Mock<ITracer> _tracer;
private IServiceCollection _serviceCollection;
private IServiceProvider _serviceProvider;
private Mock<IRequestScopedDataRepository> _repo;
public TracingHandlerFactoryTests()
{
_tracer = new Mock<IServiceTracer>();
_tracer = new Mock<ITracer>();
_serviceCollection = new ServiceCollection();
_serviceCollection.AddSingleton<IServiceTracer>(_tracer.Object);
_serviceCollection.AddSingleton<ITracer>(_tracer.Object);
_serviceProvider = _serviceCollection.BuildServiceProvider();
_repo = new Mock<IRequestScopedDataRepository>();
_factory = new TracingHandlerFactory(_serviceProvider, _repo.Object);

Loading…
Cancel
Save