diff --git a/.gitignore b/.gitignore index 7b3d14ea93..801273e9db 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ nuget.exe node_modules *.sln.ide project.lock.json +.vs/ \ No newline at end of file diff --git a/samples/ElmPageSample/HelloWorldMiddleware.cs b/samples/ElmPageSample/HelloWorldMiddleware.cs index bebf8fa26f..f870a9d210 100644 --- a/samples/ElmPageSample/HelloWorldMiddleware.cs +++ b/samples/ElmPageSample/HelloWorldMiddleware.cs @@ -1,4 +1,6 @@ -using System; +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Http; @@ -19,11 +21,13 @@ namespace ElmPageSample public async Task Invoke(HttpContext httpContext) { - using (_logger.BeginScope("C")) + using (_logger.BeginScope("Scope1")) { _logger.LogVerbose("Getting message"); - await httpContext.Response.WriteAsync("Hello World!"); + httpContext.Response.ContentType = "text/html; charset=utf-8"; + await httpContext.Response.WriteAsync( + "

Hello World!

Elm Logs"); } } } diff --git a/samples/ElmPageSample/Startup.cs b/samples/ElmPageSample/Startup.cs index f27de5b4b3..a282c11fbe 100644 --- a/samples/ElmPageSample/Startup.cs +++ b/samples/ElmPageSample/Startup.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNet.Builder; +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Builder; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.Logging; diff --git a/samples/ElmPageSample/project.json b/samples/ElmPageSample/project.json index 1f3a5a6e08..f32a35eaa4 100644 --- a/samples/ElmPageSample/project.json +++ b/samples/ElmPageSample/project.json @@ -4,8 +4,7 @@ "dependencies": { "Microsoft.AspNet.Diagnostics.Elm": "1.0.0-*", "Microsoft.AspNet.Server.IIS": "1.0.0-*", - "Microsoft.AspNet.Server.WebListener": "1.0.0-*", - "Microsoft.AspNet.StaticFiles": "1.0.0-*" + "Microsoft.AspNet.Server.WebListener": "1.0.0-*" }, "commands": { "web": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5000" diff --git a/samples/ElmPageSample/wwwroot/Readme.md b/samples/ElmPageSample/wwwroot/Readme.md new file mode 100644 index 0000000000..11abaf5374 --- /dev/null +++ b/samples/ElmPageSample/wwwroot/Readme.md @@ -0,0 +1 @@ +Sample demonstrating ElmPageMiddleware \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/ElmCaptureMiddleware.cs b/src/Microsoft.AspNet.Diagnostics.Elm/ElmCaptureMiddleware.cs index 72186fa6a1..4bfcf351da 100644 --- a/src/Microsoft.AspNet.Diagnostics.Elm/ElmCaptureMiddleware.cs +++ b/src/Microsoft.AspNet.Diagnostics.Elm/ElmCaptureMiddleware.cs @@ -1,10 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Features; using Microsoft.Framework.Logging; using Microsoft.Framework.OptionsModel; @@ -28,18 +28,20 @@ namespace Microsoft.AspNet.Diagnostics.Elm public async Task Invoke(HttpContext context) { - var requestId = Guid.NewGuid(); - using (_logger.BeginScope(string.Format("request {0}", requestId))) + using (RequestIdentifier.Ensure(context)) { - var p = ElmScope.Current; - ElmScope.Current.Context.HttpInfo = GetHttpInfo(context, requestId); - try + var requestId = context.GetFeature().TraceIdentifier; + using (_logger.BeginScope("Request: {RequestId}", requestId)) { - await _next(context); - } - finally - { - ElmScope.Current.Context.HttpInfo.StatusCode = context.Response.StatusCode; + try + { + ElmScope.Current.Context.HttpInfo = GetHttpInfo(context); + await _next(context); + } + finally + { + ElmScope.Current.Context.HttpInfo.StatusCode = context.Response.StatusCode; + } } } } @@ -48,11 +50,11 @@ namespace Microsoft.AspNet.Diagnostics.Elm /// Takes the info from the given HttpContext and copies it to an HttpInfo object /// /// The HttpInfo for the current elm context - private static HttpInfo GetHttpInfo(HttpContext context, Guid requestId) + private static HttpInfo GetHttpInfo(HttpContext context) { return new HttpInfo() { - RequestID = requestId, + RequestID = context.GetFeature().TraceIdentifier, Host = context.Request.Host, ContentType = context.Request.ContentType, Path = context.Request.Path, diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/HttpInfo.cs b/src/Microsoft.AspNet.Diagnostics.Elm/HttpInfo.cs index fc6becdb99..dee00626af 100644 --- a/src/Microsoft.AspNet.Diagnostics.Elm/HttpInfo.cs +++ b/src/Microsoft.AspNet.Diagnostics.Elm/HttpInfo.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Diagnostics.Elm { public class HttpInfo { - public Guid RequestID { get; set; } + public string RequestID { get; set; } public HostString Host { get; set; } @@ -20,7 +20,7 @@ namespace Microsoft.AspNet.Diagnostics.Elm public string Scheme { get; set; } public int StatusCode { get; set; } - + public ClaimsPrincipal User { get; set; } public string Method { get; set; } diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/HttpRequestIdentifierFeature.cs b/src/Microsoft.AspNet.Diagnostics.Elm/HttpRequestIdentifierFeature.cs new file mode 100644 index 0000000000..213c711d6b --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/HttpRequestIdentifierFeature.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http.Features; + +namespace Microsoft.AspNet.Diagnostics.Elm +{ + internal class HttpRequestIdentifierFeature : IHttpRequestIdentifierFeature + { + public string TraceIdentifier { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/RequestIdentifier.cs b/src/Microsoft.AspNet.Diagnostics.Elm/RequestIdentifier.cs new file mode 100644 index 0000000000..0ec1d2e2b2 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/RequestIdentifier.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Features; + +namespace Microsoft.AspNet.Diagnostics.Elm +{ + internal class RequestIdentifier : IDisposable + { + private readonly bool _addedFeature; + private readonly bool _updatedIdentifier; + private readonly string _originalIdentifierValue; + private readonly HttpContext _context; + private readonly IHttpRequestIdentifierFeature _feature; + + private RequestIdentifier(HttpContext context) + { + _context = context; + _feature = context.GetFeature(); + + if (_feature == null) + { + _feature = new HttpRequestIdentifierFeature() + { + TraceIdentifier = Guid.NewGuid().ToString() + }; + context.SetFeature(_feature); + _addedFeature = true; + } + else if (string.IsNullOrEmpty(_feature.TraceIdentifier)) + { + _originalIdentifierValue = _feature.TraceIdentifier; + _feature.TraceIdentifier = Guid.NewGuid().ToString(); + _updatedIdentifier = true; + } + } + + public static IDisposable Ensure(HttpContext context) + { + return new RequestIdentifier(context); + } + + public void Dispose() + { + if (_addedFeature) + { + _context.SetFeature(null); + } + else if (_updatedIdentifier) + { + _feature.TraceIdentifier = _originalIdentifierValue; + } + } + } +} diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/Views/DetailsPage.cs b/src/Microsoft.AspNet.Diagnostics.Elm/Views/DetailsPage.cs index c51c693b12..5b7705542e 100644 --- a/src/Microsoft.AspNet.Diagnostics.Elm/Views/DetailsPage.cs +++ b/src/Microsoft.AspNet.Diagnostics.Elm/Views/DetailsPage.cs @@ -241,7 +241,6 @@ WriteTo(__razor_helper_writer, Traverse(node.Children[i])); public override async Task ExecuteAsync() { Response.ContentType = "text/html; charset=utf-8"; - Response.ContentLength = null; // Clear any prior Content-Length WriteLiteral("\r\n"); WriteLiteral("\r\n"); WriteLiteral("\r\n"); diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/Views/LogPage.cs b/src/Microsoft.AspNet.Diagnostics.Elm/Views/LogPage.cs index 2de7ff4ec8..81a30ca1ec 100644 --- a/src/Microsoft.AspNet.Diagnostics.Elm/Views/LogPage.cs +++ b/src/Microsoft.AspNet.Diagnostics.Elm/Views/LogPage.cs @@ -307,7 +307,6 @@ WriteTo(__razor_helper_writer, LogRow(new LogInfo() public override async Task ExecuteAsync() { Response.ContentType = "text/html; charset=utf-8"; - Response.ContentLength = null; // Clear any prior Content-Length WriteLiteral("\r\n"); WriteLiteral("\r\n\r\n"); WriteLiteral("\r\n"); diff --git a/src/Microsoft.AspNet.Diagnostics/Views/CompilationErrorPage.cs b/src/Microsoft.AspNet.Diagnostics/Views/CompilationErrorPage.cs index 5d10cc856b..2bab1297cc 100644 --- a/src/Microsoft.AspNet.Diagnostics/Views/CompilationErrorPage.cs +++ b/src/Microsoft.AspNet.Diagnostics/Views/CompilationErrorPage.cs @@ -193,7 +193,7 @@ using Views #line hidden #line 48 "CompilationErrorPage.cshtml" - if (frame.Line != 0 && frame.ContextCode.Any()) + if (frame.Line != 0 && frame.ContextCode !=null && frame.ContextCode.Any()) { #line default diff --git a/src/Microsoft.AspNet.Diagnostics/Views/CompilationErrorPage.cshtml b/src/Microsoft.AspNet.Diagnostics/Views/CompilationErrorPage.cshtml index 143ab573cf..22d2a539a3 100644 --- a/src/Microsoft.AspNet.Diagnostics/Views/CompilationErrorPage.cshtml +++ b/src/Microsoft.AspNet.Diagnostics/Views/CompilationErrorPage.cshtml @@ -45,7 +45,7 @@

@frame.ErrorDetails

} - @if (frame.Line != 0 && frame.ContextCode.Any()) + @if (frame.Line != 0 && frame.ContextCode !=null && frame.ContextCode.Any()) {
@if (frame.PreContextCode != null) diff --git a/src/Microsoft.AspNet.Diagnostics/Views/ErrorPage.cs b/src/Microsoft.AspNet.Diagnostics/Views/ErrorPage.cs index cf8002af99..080d3a431d 100644 --- a/src/Microsoft.AspNet.Diagnostics/Views/ErrorPage.cs +++ b/src/Microsoft.AspNet.Diagnostics/Views/ErrorPage.cs @@ -355,7 +355,7 @@ using Views #line hidden #line 97 "ErrorPage.cshtml" - if (frame.Line != 0 && frame.ContextCode.Any()) + if (frame.Line != 0 && frame.ContextCode !=null && frame.ContextCode.Any()) { #line default diff --git a/src/Microsoft.AspNet.Diagnostics/Views/ErrorPage.cshtml b/src/Microsoft.AspNet.Diagnostics/Views/ErrorPage.cshtml index 03062a23e0..b6905bf89d 100644 --- a/src/Microsoft.AspNet.Diagnostics/Views/ErrorPage.cshtml +++ b/src/Microsoft.AspNet.Diagnostics/Views/ErrorPage.cshtml @@ -94,7 +94,7 @@

@frame.Function in @System.IO.Path.GetFileName(frame.File)

} - @if (frame.Line != 0 && frame.ContextCode.Any()) + @if (frame.Line != 0 && frame.ContextCode !=null && frame.ContextCode.Any()) {
@if (frame.PreContextCode != null) diff --git a/test/Microsoft.AspNet.Diagnostics.Tests/ElmMiddlewareTest.cs b/test/Microsoft.AspNet.Diagnostics.Tests/ElmMiddlewareTest.cs index d76d696a1a..f2f248f6dc 100644 --- a/test/Microsoft.AspNet.Diagnostics.Tests/ElmMiddlewareTest.cs +++ b/test/Microsoft.AspNet.Diagnostics.Tests/ElmMiddlewareTest.cs @@ -1,13 +1,18 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.IO; using System.Security.Claims; using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Diagnostics.Elm; +using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Features; +using Microsoft.AspNet.Http.Features.Internal; +using Microsoft.AspNet.Http.Internal; using Microsoft.Framework.Logging; using Microsoft.Framework.OptionsModel; #if DNX451 @@ -213,9 +218,119 @@ namespace Microsoft.AspNet.Diagnostics.Tests contextMock .Setup(c => c.Request.HasFormContentType) .Returns(true); - + var requestIdentifier = new Mock(); + requestIdentifier.Setup(f => f.TraceIdentifier).Returns(Guid.NewGuid().ToString()); + contextMock.Setup(c => c.GetFeature()) + .Returns(requestIdentifier.Object); return contextMock; } #endif + + [Fact] + public async Task SetsNewIdentifierFeature_IfNotPresentOnContext() + { + // Arrange + var context = new DefaultHttpContext(); + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(new ElmLoggerProvider(new ElmStore(), new ElmOptions())); + + // Act & Assert + var errorPageMiddleware = new ElmCaptureMiddleware((innerContext) => + { + var feature = innerContext.GetFeature(); + Assert.NotNull(feature); + Assert.False(string.IsNullOrEmpty(feature.TraceIdentifier)); + return Task.FromResult(0); + }, loggerFactory, new TestElmOptions()); + + await errorPageMiddleware.Invoke(context); + + Assert.Null(context.GetFeature()); + } + + [Fact] + public async Task UsesIdentifierFeature_IfAlreadyPresentOnContext() + { + // Arrange + var requestIdentifierFeature = new HttpRequestIdentifierFeature() + { + TraceIdentifier = Guid.NewGuid().ToString() + }; + var features = new FeatureCollection(); + features.Add(typeof(IHttpRequestFeature), new HttpRequestFeature()); + features.Add(typeof(IHttpRequestIdentifierFeature), requestIdentifierFeature); + features.Add(typeof(IHttpResponseFeature), new HttpResponseFeature()); + var context = new DefaultHttpContext(features); + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(new ElmLoggerProvider(new ElmStore(), new ElmOptions())); + + // Act & Assert + var errorPageMiddleware = new ElmCaptureMiddleware((innerContext) => + { + Assert.Same(requestIdentifierFeature, innerContext.GetFeature()); + return Task.FromResult(0); + }, loggerFactory, new TestElmOptions()); + + await errorPageMiddleware.Invoke(context); + + Assert.Same(requestIdentifierFeature, context.GetFeature()); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public async Task UpdatesTraceIdentifier_IfNullOrEmpty(string requestId) + { + // Arrange + var requestIdentifierFeature = new HttpRequestIdentifierFeature() { TraceIdentifier = requestId }; + var features = new FeatureCollection(); + features.Add(typeof(IHttpRequestIdentifierFeature), requestIdentifierFeature); + features.Add(typeof(IHttpRequestFeature), new HttpRequestFeature()); + features.Add(typeof(IHttpResponseFeature), new HttpResponseFeature()); + var context = new DefaultHttpContext(features); + var loggerFactory = new LoggerFactory(); + loggerFactory.AddProvider(new ElmLoggerProvider(new ElmStore(), new ElmOptions())); + + // Act & Assert + var errorPageMiddleware = new ElmCaptureMiddleware((innerContext) => + { + var feature = innerContext.GetFeature(); + Assert.NotNull(feature); + Assert.False(string.IsNullOrEmpty(feature.TraceIdentifier)); + return Task.FromResult(0); + }, loggerFactory, new TestElmOptions()); + + await errorPageMiddleware.Invoke(context); + + Assert.Equal(requestId, context.GetFeature().TraceIdentifier); + } + + private class TestElmOptions : IOptions + { + private readonly ElmOptions _innerOptions; + + public TestElmOptions() : + this(new ElmOptions()) + { + } + + public TestElmOptions(ElmOptions innerOptions) + { + _innerOptions = innerOptions; + } + + public ElmOptions Options + { + get + { + return _innerOptions; + } + } + + public ElmOptions GetNamedOptions(string name) + { + return _innerOptions; + } + } } }