Merged into master

This commit is contained in:
Tom Gardham-Pallister 2018-04-02 18:15:08 +01:00
commit 6817943b0a
14 changed files with 228 additions and 283 deletions

View File

@ -1,13 +1,24 @@
using Ocelot.Configuration.File; using Butterfly.Client.Tracing;
using Ocelot.Configuration.File;
using Ocelot.Requester;
namespace Ocelot.Configuration.Creator namespace Ocelot.Configuration.Creator
{ {
public class HttpHandlerOptionsCreator : IHttpHandlerOptionsCreator public class HttpHandlerOptionsCreator : IHttpHandlerOptionsCreator
{ {
private IServiceTracer _tracer;
public HttpHandlerOptionsCreator(IServiceTracer tracer)
{
_tracer = tracer;
}
public HttpHandlerOptions Create(FileReRoute fileReRoute) public HttpHandlerOptions Create(FileReRoute fileReRoute)
{ {
var useTracing = _tracer.GetType() != typeof(FakeServiceTracer) ? fileReRoute.HttpHandlerOptions.UseTracing : false;
return new HttpHandlerOptions(fileReRoute.HttpHandlerOptions.AllowAutoRedirect, return new HttpHandlerOptions(fileReRoute.HttpHandlerOptions.AllowAutoRedirect,
fileReRoute.HttpHandlerOptions.UseCookieContainer, fileReRoute.HttpHandlerOptions.UseTracing); fileReRoute.HttpHandlerOptions.UseCookieContainer, useTracing);
} }
} }
} }

View File

@ -0,0 +1,17 @@
using Microsoft.Extensions.Primitives;
using System.Linq;
namespace Ocelot.Infrastructure.Extensions
{
internal static class StringValueExtensions
{
public static string GetValue(this StringValues stringValues)
{
if (stringValues.Count == 1)
{
return stringValues;
}
return stringValues.ToArray().LastOrDefault();
}
}
}

View File

@ -0,0 +1,22 @@
using System;
namespace Ocelot.Logging
{
/// <summary>
/// Thin wrapper around the DotNet core logging framework, used to allow the scopedDataRepository to be injected giving access to the Ocelot RequestId
/// </summary>
public interface IOcelotLogger
{
void LogTrace(string message, params object[] args);
void LogDebug(string message, params object[] args);
void LogInformation(string message, params object[] args);
void LogError(string message, Exception exception);
void LogError(string message, params object[] args);
void LogCritical(string message, Exception exception);
/// <summary>
/// The name of the type the logger has been built for.
/// </summary>
string Name { get; }
}
}

View File

@ -1,27 +1,7 @@
using System; namespace Ocelot.Logging
namespace Ocelot.Logging
{ {
public interface IOcelotLoggerFactory public interface IOcelotLoggerFactory
{ {
IOcelotLogger CreateLogger<T>(); IOcelotLogger CreateLogger<T>();
} }
/// <summary>
/// Thin wrapper around the DotNet core logging framework, used to allow the scopedDataRepository to be injected giving access to the Ocelot RequestId
/// </summary>
public interface IOcelotLogger
{
void LogTrace(string message, params object[] args);
void LogDebug(string message, params object[] args);
void LogInformation(string message, params object[] args);
void LogError(string message, Exception exception);
void LogError(string message, params object[] args);
void LogCritical(string message, Exception exception);
/// <summary>
/// The name of the type the logger has been built for.
/// </summary>
string Name { get; }
}
} }

View File

@ -3,18 +3,48 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DiagnosticAdapter; using Microsoft.Extensions.DiagnosticAdapter;
using Butterfly.Client.AspNetCore; using Butterfly.Client.AspNetCore;
using Butterfly.OpenTracing; using Butterfly.OpenTracing;
using Ocelot.Middleware;
using Butterfly.Client.Tracing;
using System.Linq;
using System.Collections.Generic;
using Ocelot.Infrastructure.Extensions;
using Microsoft.Extensions.Logging;
using Ocelot.Requester;
namespace Ocelot.Logging namespace Ocelot.Logging
{ {
public class OcelotDiagnosticListener public class OcelotDiagnosticListener
{ {
private IServiceTracer _tracer;
private IOcelotLogger _logger; private IOcelotLogger _logger;
public OcelotDiagnosticListener(IOcelotLoggerFactory factory) public OcelotDiagnosticListener(IOcelotLoggerFactory factory, IServiceTracer tracer)
{ {
_tracer = tracer;
_logger = factory.CreateLogger<OcelotDiagnosticListener>(); _logger = factory.CreateLogger<OcelotDiagnosticListener>();
} }
[DiagnosticName("Ocelot.MiddlewareException")]
public virtual void OcelotMiddlewareException(Exception exception, DownstreamContext context, string name)
{
_logger.LogTrace($"Ocelot.MiddlewareException: {name}; {exception.Message}");
Event(context.HttpContext, $"Ocelot.MiddlewareStarted: {name}; {context.HttpContext.Request.Path}");
}
[DiagnosticName("Ocelot.MiddlewareStarted")]
public virtual void OcelotMiddlewareStarted(DownstreamContext context, string name)
{
_logger.LogTrace($"Ocelot.MiddlewareStarted: {name}; {context.HttpContext.Request.Path}");
Event(context.HttpContext, $"Ocelot.MiddlewareStarted: {name}; {context.HttpContext.Request.Path}");
}
[DiagnosticName("Ocelot.MiddlewareFinished")]
public virtual void OcelotMiddlewareFinished(DownstreamContext context, string name)
{
_logger.LogTrace($"OcelotMiddlewareFinished: {name}; {context.HttpContext.Request.Path}");
Event(context.HttpContext, $"OcelotMiddlewareFinished: {name}; {context.HttpContext.Request.Path}");
}
[DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareStarting")] [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareStarting")]
public virtual void OnMiddlewareStarting(HttpContext httpContext, string name) public virtual void OnMiddlewareStarting(HttpContext httpContext, string name)
{ {
@ -37,7 +67,27 @@ namespace Ocelot.Logging
private void Event(HttpContext httpContext, string @event) private void Event(HttpContext httpContext, string @event)
{ {
// Hack - if the user isnt using tracing the code gets here and will blow up on
// _tracer.Tracer.TryExtract. We already use the fake tracer for another scenario
// so sticking it here as well..I guess we need a factory for this but no idea
// how to hook that into the diagnostic framework at the moment.
if(_tracer.GetType() == typeof(FakeServiceTracer))
{
return;
}
var span = httpContext.GetSpan(); 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)); span?.Log(LogField.CreateNew().Event(@event));
} }
} }

View File

@ -65,6 +65,8 @@
rest of asp.net.. rest of asp.net..
*/ */
builder.Properties["analysis.NextMiddlewareName"] = "TransitionToOcelotMiddleware";
builder.Use(async (context, task) => builder.Use(async (context, task) =>
{ {
var downstreamContext = new DownstreamContext(context); var downstreamContext = new DownstreamContext(context);

View File

@ -3,6 +3,7 @@
// Removed code and changed RequestDelete to OcelotRequestDelete, HttpContext to DownstreamContext, removed some exception handling messages // Removed code and changed RequestDelete to OcelotRequestDelete, HttpContext to DownstreamContext, removed some exception handling messages
using System; using System;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
@ -75,7 +76,28 @@ namespace Ocelot.Middleware.Pipeline
var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs); var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);
if (parameters.Length == 1) if (parameters.Length == 1)
{ {
return (OcelotRequestDelegate)methodinfo.CreateDelegate(typeof(OcelotRequestDelegate), instance); var ocelotDelegate = (OcelotRequestDelegate)methodinfo.CreateDelegate(typeof(OcelotRequestDelegate), instance);
var diagnosticListener = (DiagnosticListener)app.ApplicationServices.GetService(typeof(DiagnosticListener));
var middlewareName = ocelotDelegate.Target.GetType().Name;
OcelotRequestDelegate wrapped = context => {
try
{
Write(diagnosticListener, "Ocelot.MiddlewareStarted", middlewareName, context);
return ocelotDelegate(context);
}
catch(Exception ex)
{
Write(diagnosticListener, "Ocelot.MiddlewareException", middlewareName, context);
throw ex;
}
finally
{
Write(diagnosticListener, "Ocelot.MiddlewareFinished", middlewareName, context);
}
};
return wrapped;
} }
var factory = Compile<object>(methodinfo, parameters); var factory = Compile<object>(methodinfo, parameters);
@ -93,6 +115,14 @@ namespace Ocelot.Middleware.Pipeline
}); });
} }
private static void Write(DiagnosticListener diagnosticListener, string message, string middlewareName, DownstreamContext context)
{
if(diagnosticListener != null)
{
diagnosticListener.Write(message, new { name = middlewareName, context = context });
}
}
public static IOcelotPipelineBuilder MapWhen(this IOcelotPipelineBuilder app, Predicate predicate, Action<IOcelotPipelineBuilder> configuration) public static IOcelotPipelineBuilder MapWhen(this IOcelotPipelineBuilder app, Predicate predicate, Action<IOcelotPipelineBuilder> configuration)
{ {
if (app == null) if (app == null)

View File

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

View File

@ -0,0 +1,17 @@
using Butterfly.Client.Tracing;
using Butterfly.OpenTracing;
namespace Ocelot.Requester
{
public class FakeServiceTracer : IServiceTracer
{
public ITracer Tracer { get; }
public string ServiceName { get; }
public string Environment { get; }
public string Identity { get; }
public ISpan Start(ISpanBuilder spanBuilder)
{
return null;
}
}
}

View File

@ -1,5 +1,4 @@
using Butterfly.Client.Tracing; using Butterfly.Client.Tracing;
using Butterfly.OpenTracing;
using Ocelot.Infrastructure.RequestData; using Ocelot.Infrastructure.RequestData;
namespace Ocelot.Requester namespace Ocelot.Requester
@ -22,16 +21,4 @@ namespace Ocelot.Requester
return new OcelotHttpTracingHandler(_tracer, _repo); return new OcelotHttpTracingHandler(_tracer, _repo);
} }
} }
public class FakeServiceTracer : IServiceTracer
{
public ITracer Tracer { get; }
public string ServiceName { get; }
public string Environment { get; }
public string Identity { get; }
public ISpan Start(ISpanBuilder spanBuilder)
{
throw new System.NotImplementedException();
}
}
} }

View File

@ -2,7 +2,7 @@
"Logging": { "Logging": {
"IncludeScopes": false, "IncludeScopes": false,
"LogLevel": { "LogLevel": {
"Default": "Debug", "Default": "Error",
"System": "Error", "System": "Error",
"Microsoft": "Error" "Microsoft": "Error"
} }

View File

@ -99,7 +99,7 @@
"HttpHandlerOptions": { "HttpHandlerOptions": {
"AllowAutoRedirect": true, "AllowAutoRedirect": true,
"UseCookieContainer": true, "UseCookieContainer": true,
"UseTracing": false "UseTracing": true
}, },
"QoSOptions": { "QoSOptions": {
"ExceptionsAllowedBeforeBreaking": 3, "ExceptionsAllowedBeforeBreaking": 3,

View File

@ -1,6 +1,10 @@
using Ocelot.Configuration; using System;
using Butterfly.Client.Tracing;
using Butterfly.OpenTracing;
using Ocelot.Configuration;
using Ocelot.Configuration.Creator; using Ocelot.Configuration.Creator;
using Ocelot.Configuration.File; using Ocelot.Configuration.File;
using Ocelot.Requester;
using Shouldly; using Shouldly;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
@ -9,13 +13,54 @@ namespace Ocelot.UnitTests.Configuration
{ {
public class HttpHandlerOptionsCreatorTests public class HttpHandlerOptionsCreatorTests
{ {
private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; private IHttpHandlerOptionsCreator _httpHandlerOptionsCreator;
private FileReRoute _fileReRoute; private FileReRoute _fileReRoute;
private HttpHandlerOptions _httpHandlerOptions; private HttpHandlerOptions _httpHandlerOptions;
private IServiceTracer _serviceTracer;
public HttpHandlerOptionsCreatorTests() public HttpHandlerOptionsCreatorTests()
{ {
_httpHandlerOptionsCreator = new HttpHandlerOptionsCreator(); _serviceTracer = new FakeServiceTracer();
_httpHandlerOptionsCreator = new HttpHandlerOptionsCreator(_serviceTracer);
}
[Fact]
public void should_not_use_tracing_if_fake_tracer_registered()
{
var fileReRoute = new FileReRoute
{
HttpHandlerOptions = new FileHttpHandlerOptions
{
UseTracing = true
}
};
var expectedOptions = new HttpHandlerOptions(false, false, false);
this.Given(x => GivenTheFollowing(fileReRoute))
.When(x => WhenICreateHttpHandlerOptions())
.Then(x => ThenTheFollowingOptionsReturned(expectedOptions))
.BDDfy();
}
[Fact]
public void should_use_tracing_if_real_tracer_registered()
{
var fileReRoute = new FileReRoute
{
HttpHandlerOptions = new FileHttpHandlerOptions
{
UseTracing = true
}
};
var expectedOptions = new HttpHandlerOptions(false, false, true);
this.Given(x => GivenTheFollowing(fileReRoute))
.And(x => GivenARealTracer())
.When(x => WhenICreateHttpHandlerOptions())
.Then(x => ThenTheFollowingOptionsReturned(expectedOptions))
.BDDfy();
} }
[Fact] [Fact]
@ -68,5 +113,27 @@ namespace Ocelot.UnitTests.Configuration
_httpHandlerOptions.UseCookieContainer.ShouldBe(expected.UseCookieContainer); _httpHandlerOptions.UseCookieContainer.ShouldBe(expected.UseCookieContainer);
_httpHandlerOptions.UseTracing.ShouldBe(expected.UseTracing); _httpHandlerOptions.UseTracing.ShouldBe(expected.UseTracing);
} }
private void GivenARealTracer()
{
var tracer = new RealTracer();
_httpHandlerOptionsCreator = new HttpHandlerOptionsCreator(tracer);
}
class RealTracer : IServiceTracer
{
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 ISpan Start(ISpanBuilder spanBuilder)
{
throw new NotImplementedException();
}
}
} }
} }

View File

@ -1,239 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Consul;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Ocelot.Configuration.File;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests.Websockets
{
public class WebSocketsProxyMiddlewareTests : IDisposable
{
private IWebHost _firstDownstreamHost;
private readonly List<string> _firstRecieved;
private WebHostBuilder _ocelotBuilder;
private IWebHost _ocelotHost;
public WebSocketsProxyMiddlewareTests()
{
_firstRecieved = new List<string>();
}
[Fact]
public async Task should_proxy_websocket_input_to_downstream_service()
{
var downstreamPort = 5001;
var downstreamHost = "localhost";
var config = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
UpstreamPathTemplate = "/",
DownstreamPathTemplate = "/ws",
DownstreamScheme = "ws",
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = downstreamHost,
Port = downstreamPort
}
}
}
}
};
this.Given(_ => GivenThereIsAConfiguration(config))
.And(_ => StartFakeOcelotWithWebSockets())
.And(_ => StartFakeDownstreamService($"http://{downstreamHost}:{downstreamPort}", "/ws"))
.When(_ => StartClient("ws://localhost:5000/"))
.Then(_ => _firstRecieved.Count.ShouldBe(10))
.BDDfy();
}
public void Dispose()
{
_firstDownstreamHost?.Dispose();
}
public async Task StartFakeOcelotWithWebSockets()
{
_ocelotBuilder = new WebHostBuilder();
_ocelotBuilder.ConfigureServices(s =>
{
s.AddSingleton(_ocelotBuilder);
s.AddOcelot();
});
_ocelotBuilder.UseKestrel()
.UseUrls("http://localhost:5000")
.UseContentRoot(Directory.GetCurrentDirectory())
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
var env = hostingContext.HostingEnvironment;
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
config.AddJsonFile("configuration.json");
config.AddEnvironmentVariables();
})
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
})
.Configure(app =>
{
app.UseWebSockets();
app.UseOcelot().Wait();
})
.UseIISIntegration();
_ocelotHost = _ocelotBuilder.Build();
await _ocelotHost.StartAsync();
}
public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration)
{
var configurationPath = Path.Combine(AppContext.BaseDirectory, "configuration.json");
var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration);
if (File.Exists(configurationPath))
{
File.Delete(configurationPath);
}
File.WriteAllText(configurationPath, jsonConfiguration);
}
private async Task StartFakeDownstreamService(string url, string path)
{
_firstDownstreamHost = new WebHostBuilder()
.ConfigureServices(s => { }).UseKestrel()
.UseUrls(url)
.UseContentRoot(Directory.GetCurrentDirectory())
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
var env = hostingContext.HostingEnvironment;
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
config.AddEnvironmentVariables();
})
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
})
.Configure(app =>
{
app.UseWebSockets();
app.Use(async (context, next) =>
{
if (context.Request.Path == path)
{
if (context.WebSockets.IsWebSocketRequest)
{
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
await Echo(webSocket);
}
else
{
context.Response.StatusCode = 400;
}
}
else
{
await next();
}
});
})
.UseIISIntegration().Build();
await _firstDownstreamHost.StartAsync();
}
private async Task StartClient(string url)
{
var client = new ClientWebSocket();
await client.ConnectAsync(new Uri(url), CancellationToken.None);
var sending = Task.Run(async () =>
{
string line = "test";
for (int i = 0; i < 10; i++)
{
var bytes = Encoding.UTF8.GetBytes(line);
await client.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true,
CancellationToken.None);
await Task.Delay(10);
}
await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
});
var receiving = Task.Run(async () =>
{
var buffer = new byte[1024 * 4];
while (true)
{
var result = await client.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Text)
{
_firstRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count));
}
else if (result.MessageType == WebSocketMessageType.Close)
{
await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
break;
}
}
});
await Task.WhenAll(sending, receiving);
}
private async Task Echo(WebSocket webSocket)
{
try
{
var buffer = new byte[1024 * 4];
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
while (!result.CloseStatus.HasValue)
{
await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
}
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
}