diff --git a/DiagnosticsPages.sln b/DiagnosticsPages.sln index f3c669a4..78343bcd 100644 --- a/DiagnosticsPages.sln +++ b/DiagnosticsPages.sln @@ -28,6 +28,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Diagnostic EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ErrorHandlerSample", "samples\ErrorHandlerSample\ErrorHandlerSample.kproj", "{427CDB36-78B0-4583-9EBC-7F283DE60355}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Diagnostics.Elm", "src\Microsoft.AspNet.Diagnostics.Elm\Microsoft.AspNet.Diagnostics.Elm.kproj", "{624B0019-956A-4157-B008-270C5B229553}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -108,6 +110,18 @@ Global {427CDB36-78B0-4583-9EBC-7F283DE60355}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {427CDB36-78B0-4583-9EBC-7F283DE60355}.Release|Mixed Platforms.Build.0 = Release|Any CPU {427CDB36-78B0-4583-9EBC-7F283DE60355}.Release|x86.ActiveCfg = Release|Any CPU + {624B0019-956A-4157-B008-270C5B229553}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {624B0019-956A-4157-B008-270C5B229553}.Debug|Any CPU.Build.0 = Debug|Any CPU + {624B0019-956A-4157-B008-270C5B229553}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {624B0019-956A-4157-B008-270C5B229553}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {624B0019-956A-4157-B008-270C5B229553}.Debug|x86.ActiveCfg = Debug|Any CPU + {624B0019-956A-4157-B008-270C5B229553}.Debug|x86.Build.0 = Debug|Any CPU + {624B0019-956A-4157-B008-270C5B229553}.Release|Any CPU.ActiveCfg = Release|Any CPU + {624B0019-956A-4157-B008-270C5B229553}.Release|Any CPU.Build.0 = Release|Any CPU + {624B0019-956A-4157-B008-270C5B229553}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {624B0019-956A-4157-B008-270C5B229553}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {624B0019-956A-4157-B008-270C5B229553}.Release|x86.ActiveCfg = Release|Any CPU + {624B0019-956A-4157-B008-270C5B229553}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -120,5 +134,6 @@ Global {CD62A191-39F5-4C86-BC1D-7731085120F5} = {ACAA0157-A8C4-4152-93DE-90CCDF304087} {994351B4-7B2A-4139-8B72-72C5BB5CC618} = {2AF90579-B118-4583-AE88-672EFACB5BC4} {427CDB36-78B0-4583-9EBC-7F283DE60355} = {ACAA0157-A8C4-4152-93DE-90CCDF304087} + {624B0019-956A-4157-B008-270C5B229553} = {509A6F36-AD80-4A18-B5B1-717D38DFF29D} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/ActivityContext.cs b/src/Microsoft.AspNet.Diagnostics.Elm/ActivityContext.cs new file mode 100644 index 00000000..41833c39 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/ActivityContext.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Diagnostics.Elm +{ + public class ActivityContext + { + public Guid Id { get; set; } + + public HttpInfo HttpInfo { get; set; } + + public ScopeNode Root { get; set; } + + public DateTimeOffset Time { get; set; } + + public bool IsCollapsed { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/ElmCaptureMiddleware.cs b/src/Microsoft.AspNet.Diagnostics.Elm/ElmCaptureMiddleware.cs new file mode 100644 index 00000000..a2bd2f96 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/ElmCaptureMiddleware.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Framework.Logging; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Diagnostics.Elm +{ + /// + /// Enables the Elm logging service. + /// + public class ElmCaptureMiddleware + { + private readonly RequestDelegate _next; + private readonly ElmOptions _options; + private readonly ILogger _logger; + + public ElmCaptureMiddleware(RequestDelegate next, ILoggerFactory factory, IOptions options) + { + _next = next; + _options = options.Options; + _logger = factory.Create(); + } + + public async Task Invoke(HttpContext context) + { + var requestId = Guid.NewGuid(); + using (_logger.BeginScope(string.Format("request {0}", requestId))) + { + var p = ElmScope.Current; + ElmScope.Current.Context.HttpInfo = GetHttpInfo(context, requestId); + try + { + await _next(context); + } + finally + { + ElmScope.Current.Context.HttpInfo.StatusCode = context.Response.StatusCode; + } + } + } + + /// + /// 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) + { + return new HttpInfo() + { + RequestID = requestId, + Host = context.Request.Host, + ContentType = context.Request.ContentType, + Path = context.Request.Path, + Scheme = context.Request.Scheme, + StatusCode = context.Response.StatusCode, + User = context.User, + Method = context.Request.Method, + Protocol = context.Request.Protocol, + Headers = context.Request.Headers, + Query = context.Request.QueryString, + Cookies = context.Request.Cookies + }; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/ElmExtensions.cs b/src/Microsoft.AspNet.Diagnostics.Elm/ElmExtensions.cs new file mode 100644 index 00000000..29811f5f --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/ElmExtensions.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Jetbrains.Annotations; +using Microsoft.AspNet.Diagnostics.Elm; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; + +namespace Microsoft.AspNet.Builder +{ + public static class ElmExtensions + { + /// + /// Enables the Elm logging service, which can be accessed via the . + /// + public static IApplicationBuilder UseElmCapture([NotNull] this IApplicationBuilder builder) + { + // add the elm provider to the factory here so the logger can start capturing logs immediately + var factory = builder.ApplicationServices.GetRequiredService(); + var store = builder.ApplicationServices.GetRequiredService(); + var options = builder.ApplicationServices.GetService>(); + factory.AddProvider(new ElmLoggerProvider(store, options?.Options ?? new ElmOptions())); + + return builder.UseMiddleware(); + } + + /// + /// Enables viewing logs captured by the . + /// + public static IApplicationBuilder UseElmPage([NotNull] this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + + /// + /// Registers an and configures . + /// + public static IServiceCollection AddElm([NotNull] this IServiceCollection services, Action configureOptions = null) + { + services.AddSingleton(); // registering the service so it can be injected into constructors + return services.Configure(configureOptions ?? (o => { })); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/ElmLogger.cs b/src/Microsoft.AspNet.Diagnostics.Elm/ElmLogger.cs new file mode 100644 index 00000000..9238d6d6 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/ElmLogger.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Framework.Logging; + +namespace Microsoft.AspNet.Diagnostics.Elm +{ + public class ElmLogger : ILogger + { + private readonly string _name; + private readonly ElmOptions _options; + private readonly ElmStore _store; + + public ElmLogger(string name, ElmOptions options, ElmStore store) + { + _name = name; + _options = options; + _store = store; + } + + public void Write(LogLevel logLevel, int eventId, object state, Exception exception, + Func formatter) + { + if (!IsEnabled(logLevel) || (state == null && exception == null)) + { + return; + } + LogInfo info = new LogInfo() + { + ActivityContext = GetCurrentActivityContext(), + Name = _name, + EventID = eventId, + Severity = logLevel, + Exception = exception, + State = state, + Message = formatter == null ? state.ToString() : formatter(state, exception), + Time = DateTimeOffset.UtcNow + }; + if (ElmScope.Current != null) + { + ElmScope.Current.Node.Messages.Add(info); + } + // The log does not belong to any scope - create a new context for it + else + { + var context = GetNewActivityContext(); + context.Id = Guid.Empty; // mark as a non-scope log + context.Root = new ScopeNode(); + context.Root.Messages.Add(info); + _store.AddActivity(context); + } + } + + public bool IsEnabled(LogLevel logLevel) + { + return _options.Filter(_name, logLevel); + } + + public IDisposable BeginScope(object state) + { + var scope = new ElmScope(_name, state); + scope.Context = ElmScope.Current?.Context ?? GetNewActivityContext(); + return ElmScope.Push(scope, _store); + } + + private ActivityContext GetNewActivityContext() + { + return new ActivityContext() + { + Id = Guid.NewGuid(), + Time = DateTimeOffset.UtcNow + }; + } + + private ActivityContext GetCurrentActivityContext() + { + return ElmScope.Current?.Context ?? GetNewActivityContext(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/ElmLoggerProvider.cs b/src/Microsoft.AspNet.Diagnostics.Elm/ElmLoggerProvider.cs new file mode 100644 index 00000000..1c74128e --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/ElmLoggerProvider.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Jetbrains.Annotations; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Diagnostics.Elm +{ + public class ElmLoggerProvider : ILoggerProvider + { + private readonly ElmStore _store; + private readonly ElmOptions _options; + + public ElmLoggerProvider([NotNull] ElmStore store, [NotNull] ElmOptions options) + { + _store = store; + _options = options; + } + + public ILogger Create(string name) + { + return new ElmLogger(name, _options, _store); + } + } +} diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/ElmOptions.cs b/src/Microsoft.AspNet.Diagnostics.Elm/ElmOptions.cs new file mode 100644 index 00000000..395921e1 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/ElmOptions.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Framework.Logging; + +namespace Microsoft.AspNet.Diagnostics.Elm +{ + /// + /// Options for ElmMiddleware + /// + public class ElmOptions + { + /// + /// Specifies the path to view the logs. + /// + public PathString Path { get; set; } = new PathString("/Elm"); + + /// + /// Determines whether log statements should be logged based on the name of the logger + /// and the of the message. + /// + public Func Filter { get; set; } = (name, level) => true; + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/ElmPageMiddleware.cs b/src/Microsoft.AspNet.Diagnostics.Elm/ElmPageMiddleware.cs new file mode 100644 index 00000000..9eec2658 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/ElmPageMiddleware.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Diagnostics.Elm.Views; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +using System.Linq; + +namespace Microsoft.AspNet.Diagnostics.Elm +{ + /// + /// Enables viewing logs captured by the . + /// + public class ElmPageMiddleware + { + private readonly RequestDelegate _next; + private readonly ElmOptions _options; + private readonly ElmStore _store; + + public ElmPageMiddleware(RequestDelegate next, IOptions options, ElmStore store) + { + _next = next; + _options = options.Options; + _store = store; + } + + public async Task Invoke(HttpContext context) + { + if (!context.Request.Path.StartsWithSegments(_options.Path)) + { + await _next(context); + return; + } + + var options = ParseParams(context); + if (context.Request.Path == _options.Path) + { + RenderMainLogPage(options, context); + } + else + { + RenderRequestDetailsPage(options, context); + } + } + + private async void RenderMainLogPage(ViewOptions options, HttpContext context) + { + var model = new LogPageModel() + { + // sort so most recent logs are first + Activities = _store.GetActivities(), + Options = options, + Path = _options.Path + }; + var logPage = new LogPage(model); + + await logPage.ExecuteAsync(context); + } + + private async void RenderRequestDetailsPage(ViewOptions options, HttpContext context) + { + var parts = context.Request.Path.Value.Split('/'); + var id = Guid.Empty; + if (!Guid.TryParse(parts[parts.Length - 1], out id)) + { + context.Response.StatusCode = 400; + await context.Response.WriteAsync("Invalid Request Id"); + return; + } + var model = new RequestPageModel() + { + RequestID = id, + Activity = _store.GetActivities().Where(a => a.HttpInfo?.RequestID == id).FirstOrDefault(), + Options = options + }; + var requestPage = new RequestPage(model); + await requestPage.ExecuteAsync(context); + } + + private ViewOptions ParseParams(HttpContext context) + { + var options = new ViewOptions() + { + MinLevel = LogLevel.Verbose, + NamePrefix = string.Empty + }; + if (context.Request.Query.ContainsKey("level")) + { + var minLevel = options.MinLevel; + if (Enum.TryParse(context.Request.Query["level"], out minLevel)) + { + options.MinLevel = minLevel; + } + } + if (context.Request.Query.ContainsKey("name")) + { + var namePrefix = context.Request.Query.GetValues("name")[0]; + options.NamePrefix = namePrefix; + } + return options; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/ElmScope.cs b/src/Microsoft.AspNet.Diagnostics.Elm/ElmScope.cs new file mode 100644 index 00000000..a1c65aed --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/ElmScope.cs @@ -0,0 +1,113 @@ +using System; +#if ASPNET50 +using System.Runtime.Remoting; +using System.Runtime.Remoting.Messaging; +#endif +using System.Threading; +using Jetbrains.Annotations; + +namespace Microsoft.AspNet.Diagnostics.Elm +{ + public class ElmScope + { + private readonly string _name; + private readonly object _state; + + public ElmScope(string name, object state) + { + _name = name; + _state = state; + } + + public ActivityContext Context { get; set; } + + public ElmScope Parent { get; set; } + + public ScopeNode Node { get; set; } + +#if ASPNET50 + private static string FieldKey = typeof(ElmScope).FullName + ".Value"; + public static ElmScope Current + { + get + { + var handle = CallContext.LogicalGetData(FieldKey) as ObjectHandle; + + if (handle == null) + { + return default(ElmScope); + } + + return (ElmScope)handle.Unwrap(); + } + set + { + CallContext.LogicalSetData(FieldKey, new ObjectHandle(value)); + } + } +#else + private static AsyncLocal _value = new AsyncLocal(); + public static ElmScope Current + { + set + { + _value.Value = value; + } + get + { + return _value.Value; + } + } +#endif + + public static IDisposable Push([NotNull] ElmScope scope, [NotNull] ElmStore store) + { + var temp = Current; + Current = scope; + Current.Parent = temp; + + Current.Node = new ScopeNode() + { + StartTime = DateTimeOffset.UtcNow, + State = Current._state, + Name = Current._name + }; + + if (Current.Parent != null) + { + Current.Node.Parent = Current.Parent.Node; + Current.Parent.Node.Children.Add(Current.Node); + } + else + { + Current.Context.Root = Current.Node; + store.AddActivity(Current.Context); + } + + return new DisposableAction(() => + { + Current.Node.EndTime = DateTimeOffset.UtcNow; + Current = Current.Parent; + }); + } + + private class DisposableAction : IDisposable + { + private Action _action; + + public DisposableAction(Action action) + { + _action = action; + } + + public void Dispose() + { + if (_action != null) + { + _action.Invoke(); + _action = null; + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/ElmStore.cs b/src/Microsoft.AspNet.Diagnostics.Elm/ElmStore.cs new file mode 100644 index 00000000..b328d80e --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/ElmStore.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic; +using System.Linq; +using Jetbrains.Annotations; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Diagnostics.Elm +{ + public class ElmStore + { + private const int Capacity = 200; + + private LinkedList Activities { get; set; } = new LinkedList(); + + /// + /// Returns an IEnumerable of the contexts of the logs. + /// + /// An IEnumerable of objects where each context stores + /// information about a top level scope. + public IEnumerable GetActivities() + { + for (var context = Activities.First; context != null; context = context.Next) + { + if (!context.Value.IsCollapsed && CollapseActivityContext(context.Value)) + { + Activities.Remove(context); + } + } + return Activities; + } + + /// + /// Adds a new to the store. + /// + /// The context to be added to the store. + public void AddActivity([NotNull] ActivityContext activity) + { + lock (Activities) + { + Activities.AddLast(activity); + while (Count() > Capacity) + { + Activities.RemoveFirst(); + } + } + } + + /// + /// Returns the total number of logs in all activities in the store + /// + /// The total log count + public int Count() + { + return Activities.Sum(a => Count(a.Root)); + } + + private int Count(ScopeNode node) + { + if (node == null) + { + return 0; + } + var sum = node.Messages.Count; + foreach (var child in node.Children) + { + sum += Count(child); + } + return sum; + } + + /// + /// Removes any nodes on the context's scope tree that doesn't have any logs + /// This may occur as a result of the filters turned on + /// + /// The context who's node should be condensed + /// true if the node has been condensed to null, false otherwise + private bool CollapseActivityContext(ActivityContext context) + { + context.Root = CollapseHelper(context.Root); + context.IsCollapsed = true; + return context.Root == null; + } + + private ScopeNode CollapseHelper(ScopeNode node) + { + if (node == null) + { + return node; + } + for (int i = 0; i < node.Children.Count; i++) + { + node.Children[i] = CollapseHelper(node.Children[i]); + } + node.Children.RemoveAll(c => c == null); + if (node.Children.Count == 0 && node.Messages.Count == 0) + { + return null; + } + else + { + return node; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/HttpInfo.cs b/src/Microsoft.AspNet.Diagnostics.Elm/HttpInfo.cs new file mode 100644 index 00000000..8f67ce99 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/HttpInfo.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Security.Claims; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Diagnostics.Elm +{ + public class HttpInfo + { + public Guid RequestID { get; set; } + + public HostString Host { get; set; } + + public PathString Path { get; set; } + + public string ContentType { get; set; } + + public string Scheme { get; set; } + + public int StatusCode { get; set; } + + public ClaimsPrincipal User { get; set; } + + public string Method { get; set; } + + public string Protocol { get; set; } + + public IHeaderDictionary Headers { get; set; } + + public QueryString Query { get; set; } + + public IReadableStringCollection Cookies { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/LogInfo.cs b/src/Microsoft.AspNet.Diagnostics.Elm/LogInfo.cs new file mode 100644 index 00000000..6e3e477a --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/LogInfo.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Framework.Logging; + +namespace Microsoft.AspNet.Diagnostics.Elm +{ + public class LogInfo + { + public ActivityContext ActivityContext { get; set; } + + public string Name { get; set; } + + public object State { get; set; } + + public Exception Exception { get; set; } + + public string Message { get; set; } + + public LogLevel Severity { get; set; } + + public int EventID { get; set; } + + public DateTimeOffset Time { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/Microsoft.AspNet.Diagnostics.Elm.kproj b/src/Microsoft.AspNet.Diagnostics.Elm/Microsoft.AspNet.Diagnostics.Elm.kproj new file mode 100644 index 00000000..06e8c5e0 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/Microsoft.AspNet.Diagnostics.Elm.kproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 624b0019-956a-4157-b008-270c5b229553 + Microsoft.AspNet.Diagnostics.Elm + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/NotNullAttribute.cs b/src/Microsoft.AspNet.Diagnostics.Elm/NotNullAttribute.cs new file mode 100644 index 00000000..8f7ae6c0 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/NotNullAttribute.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Jetbrains.Annotations +{ + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + internal sealed class NotNullAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/ScopeNode.cs b/src/Microsoft.AspNet.Diagnostics.Elm/ScopeNode.cs new file mode 100644 index 00000000..a55ed907 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/ScopeNode.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Diagnostics.Elm +{ + public class ScopeNode + { + public ScopeNode Parent { get; set; } + + public List Children { get; private set; } = new List(); + + public List Messages { get; private set; } = new List(); + + public DateTimeOffset StartTime { get; set; } + + public DateTimeOffset EndTime { get; set; } + + public object State { get; set; } + + public string Name { get; set; } + + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/ViewOptions.cs b/src/Microsoft.AspNet.Diagnostics.Elm/ViewOptions.cs new file mode 100644 index 00000000..a8e63025 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/ViewOptions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Diagnostics.Elm +{ + /// + /// Options for viewing elm logs. + /// + public class ViewOptions + { + /// + /// The minimum of logs shown on the elm page. + /// + public LogLevel MinLevel { get; set; } + + /// + /// The prefix for the logger names of logs shown on the elm page. + /// + public string NamePrefix { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/Views/LogPage.cs b/src/Microsoft.AspNet.Diagnostics.Elm/Views/LogPage.cs new file mode 100644 index 00000000..9f625710 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/Views/LogPage.cs @@ -0,0 +1,665 @@ +namespace Microsoft.AspNet.Diagnostics.Elm.Views +{ +#line 1 "LogPage.cshtml" +using System + +#line default +#line hidden + ; +#line 2 "LogPage.cshtml" +using System.Globalization + +#line default +#line hidden + ; +#line 3 "LogPage.cshtml" +using System.Linq + +#line default +#line hidden + ; +#line 4 "LogPage.cshtml" +using Microsoft.AspNet.Diagnostics.Elm.Views + +#line default +#line hidden + ; +#line 5 "LogPage.cshtml" +using Microsoft.AspNet.Diagnostics.Elm + +#line default +#line hidden + ; +#line 6 "LogPage.cshtml" +using Microsoft.AspNet.Diagnostics.Views + +#line default +#line hidden + ; +#line 7 "LogPage.cshtml" +using Microsoft.Framework.Logging + +#line default +#line hidden + ; + using System.Threading.Tasks; + + public class LogPage : Microsoft.AspNet.Diagnostics.Views.BaseView + { +public HelperResult +#line 20 "LogPage.cshtml" +LogRow(LogInfo log, int level) { + +#line default +#line hidden + return new HelperResult((__razor_helper_writer) => { +#line 20 "LogPage.cshtml" + + if (log.Severity >= Model.Options.MinLevel && + (string.IsNullOrEmpty(Model.Options.NamePrefix) || log.Name.StartsWith(Model.Options.NamePrefix, StringComparison.Ordinal))) + { + +#line default +#line hidden + + WriteLiteralTo(__razor_helper_writer, " \r\n "); +#line 25 "LogPage.cshtml" +WriteTo(__razor_helper_writer, string.Format("{0:MM/dd/yy}", log.Time)); + +#line default +#line hidden + WriteLiteralTo(__razor_helper_writer, "\r\n "); +#line 26 "LogPage.cshtml" +WriteTo(__razor_helper_writer, string.Format("{0:H:mm:ss}", log.Time)); + +#line default +#line hidden + WriteLiteralTo(__razor_helper_writer, "\r\n (log.Name, 844), false)); + WriteLiteralTo(__razor_helper_writer, ">"); +#line 27 "LogPage.cshtml" + WriteTo(__razor_helper_writer, log.Name); + +#line default +#line hidden + WriteLiteralTo(__razor_helper_writer, "\r\n (log.Severity.ToString().ToLowerInvariant(), 894), false)); + WriteLiteralTo(__razor_helper_writer, ">"); +#line 28 "LogPage.cshtml" + WriteTo(__razor_helper_writer, log.Severity); + +#line default +#line hidden + WriteLiteralTo(__razor_helper_writer, "\r\n (log.Message, 982), false)); + WriteLiteralTo(__razor_helper_writer, ">\r\n"); +#line 30 "LogPage.cshtml" + + +#line default +#line hidden + +#line 30 "LogPage.cshtml" + for (var i = 0; i < level; i++) + { + +#line default +#line hidden + + WriteLiteralTo(__razor_helper_writer, " \r\n"); +#line 33 "LogPage.cshtml" + } + +#line default +#line hidden + + WriteLiteralTo(__razor_helper_writer, " "); +#line 34 "LogPage.cshtml" +WriteTo(__razor_helper_writer, log.Message); + +#line default +#line hidden + WriteLiteralTo(__razor_helper_writer, "\r\n \r\n (log.Exception, 1205), false)); + WriteLiteralTo(__razor_helper_writer, ">"); +#line 36 "LogPage.cshtml" + WriteTo(__razor_helper_writer, log.Exception); + +#line default +#line hidden + WriteLiteralTo(__razor_helper_writer, "\r\n \r\n"); +#line 38 "LogPage.cshtml" + } + +#line default +#line hidden + + } + ); +#line 39 "LogPage.cshtml" +} + +#line default +#line hidden + +public HelperResult +#line 41 "LogPage.cshtml" +Traverse(ScopeNode node, int level) +{ + +#line default +#line hidden + return new HelperResult((__razor_helper_writer) => { +#line 42 "LogPage.cshtml" + + // print start of scope + + +#line default +#line hidden + +#line 44 "LogPage.cshtml" +WriteTo(__razor_helper_writer, LogRow(new LogInfo() + { + Name = node.Name, + Time = node.StartTime, + Severity = LogLevel.Verbose, + Message = "Beginning " + node.State, + }, level)); + +#line default +#line hidden +#line 50 "LogPage.cshtml" + ; + var messageIndex = 0; + var childIndex = 0; + while (messageIndex < node.Messages.Count && childIndex < node.Children.Count) + { + if (node.Messages[messageIndex].Time < node.Children[childIndex].StartTime) + { + + +#line default +#line hidden + +#line 57 "LogPage.cshtml" +WriteTo(__razor_helper_writer, LogRow(node.Messages[messageIndex], level)); + +#line default +#line hidden +#line 57 "LogPage.cshtml" + + messageIndex++; + } + else + { + + +#line default +#line hidden + +#line 62 "LogPage.cshtml" +WriteTo(__razor_helper_writer, Traverse(node.Children[childIndex], level + 1)); + +#line default +#line hidden +#line 62 "LogPage.cshtml" + + childIndex++; + } + } + if (messageIndex < node.Messages.Count) + { + for (var i = messageIndex; i < node.Messages.Count; i++) + { + + +#line default +#line hidden + +#line 70 "LogPage.cshtml" +WriteTo(__razor_helper_writer, LogRow(node.Messages[i], level)); + +#line default +#line hidden +#line 70 "LogPage.cshtml" + + } + } + else + { + for (var i = childIndex; i < node.Children.Count; i++) + { + + +#line default +#line hidden + +#line 77 "LogPage.cshtml" +WriteTo(__razor_helper_writer, Traverse(node.Children[i], level + 1)); + +#line default +#line hidden +#line 77 "LogPage.cshtml" + + } + } + // print end of scope + + +#line default +#line hidden + +#line 81 "LogPage.cshtml" +WriteTo(__razor_helper_writer, LogRow(new LogInfo() + { + Name = node.Name, + Time = node.EndTime, + Severity = LogLevel.Verbose, + Message = string.Format("Completed {0} in {1}ms", node.State, node.EndTime - node.StartTime) + }, level)); + +#line default +#line hidden +#line 87 "LogPage.cshtml" + ; + +#line default +#line hidden + + } + ); +#line 88 "LogPage.cshtml" +} + +#line default +#line hidden + +#line 10 "LogPage.cshtml" + + public LogPage(LogPageModel model) + { + Model = model; + } + + public LogPageModel Model { get; set; } + +#line default +#line hidden + #line hidden + public LogPage() + { + } + + #pragma warning disable 1998 + public override async Task ExecuteAsync() + { + WriteLiteral("\r\n"); + WriteLiteral("\r\n\r\n"); + WriteLiteral("\r\n"); + WriteLiteral(@" + + + + + ASP.NET Logs + + + +

ASP.NET Logs

+
+ \r\n (Model.Options.NamePrefix, 3492), false)); + WriteLiteral(@" /> + +
+ + + + + + + + + + + + + + + + +"); +#line 136 "LogPage.cshtml" + + +#line default +#line hidden + +#line 136 "LogPage.cshtml" + foreach (var activity in Model.Activities.Reverse()) + { + +#line default +#line hidden + + WriteLiteral(" \r\n \r\n"); +#line 140 "LogPage.cshtml" + + +#line default +#line hidden + +#line 140 "LogPage.cshtml" + + if (activity.HttpInfo != null) + { + var requestPath = Model.Path.Value + "/" + activity.HttpInfo.RequestID; + +#line default +#line hidden + + WriteLiteral(" \r\n \r\n \r\n"); +#line 147 "LogPage.cshtml" + } + else + { + +#line default +#line hidden + + WriteLiteral(" \r\n"); +#line 151 "LogPage.cshtml" + } + + +#line default +#line hidden + + WriteLiteral(@" + \r\n \r\n \r\n"); +#line 180 "LogPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral("
PathHostStatus CodeLogs
(requestPath, 4407), false)); + WriteAttribute("title", Tuple.Create(" title=\"", 4420), Tuple.Create("\"", 4451), + Tuple.Create(Tuple.Create("", 4428), Tuple.Create(activity.HttpInfo.Path, 4428), false)); + WriteLiteral(">"); +#line 144 "LogPage.cshtml" + Write(activity.HttpInfo.Path); + +#line default +#line hidden + WriteLiteral(""); +#line 145 "LogPage.cshtml" + Write(activity.HttpInfo.Host); + +#line default +#line hidden + WriteLiteral(""); +#line 146 "LogPage.cshtml" + Write(activity.HttpInfo.StatusCode); + +#line default +#line hidden + WriteLiteral(""); +#line 150 "LogPage.cshtml" + Write(activity.Root.State); + +#line default +#line hidden + WriteLiteral(" + + + + + + + + + + + + +"); +#line 166 "LogPage.cshtml" + + +#line default +#line hidden + +#line 166 "LogPage.cshtml" + if (activity.Id.Equals(Guid.Empty)) + { + // message not within a scope + + +#line default +#line hidden + +#line 169 "LogPage.cshtml" + Write(LogRow(activity.Root.Messages.FirstOrDefault(), 0)); + +#line default +#line hidden +#line 169 "LogPage.cshtml" + + } + else + { + + +#line default +#line hidden + +#line 173 "LogPage.cshtml" + Write(Traverse(activity.Root, 0)); + +#line default +#line hidden +#line 173 "LogPage.cshtml" + + } + +#line default +#line hidden + + WriteLiteral(" \r\n
DateTimeNameSeverityStateError
\r\n " + +"
\r\n\r\n"); + } + #pragma warning restore 1998 + } +} diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/Views/LogPage.cshtml b/src/Microsoft.AspNet.Diagnostics.Elm/Views/LogPage.cshtml new file mode 100644 index 00000000..da02a197 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/Views/LogPage.cshtml @@ -0,0 +1,183 @@ +@using System +@using System.Globalization +@using System.Linq +@using Microsoft.AspNet.Diagnostics.Elm.Views +@using Microsoft.AspNet.Diagnostics.Elm +@using Microsoft.AspNet.Diagnostics.Views +@using Microsoft.Framework.Logging + +@functions +{ + public LogPage(LogPageModel model) + { + Model = model; + } + + public LogPageModel Model { get; set; } +} + +@* writes one log row indented by the given level *@ +@helper LogRow(LogInfo log, int level) { + if (log.Severity >= Model.Options.MinLevel && + (string.IsNullOrEmpty(Model.Options.NamePrefix) || log.Name.StartsWith(Model.Options.NamePrefix, StringComparison.Ordinal))) + { + + @string.Format("{0:MM/dd/yy}", log.Time) + @string.Format("{0:H:mm:ss}", log.Time) + @log.Name + @log.Severity + + @for (var i = 0; i < level; i++) + { + + } + @log.Message + + @log.Exception + + } +} + +@helper Traverse(ScopeNode node, int level) +{ + // print start of scope + @LogRow(new LogInfo() + { + Name = node.Name, + Time = node.StartTime, + Severity = LogLevel.Verbose, + Message = "Beginning " + node.State, + }, level); + var messageIndex = 0; + var childIndex = 0; + while (messageIndex < node.Messages.Count && childIndex < node.Children.Count) + { + if (node.Messages[messageIndex].Time < node.Children[childIndex].StartTime) + { + @LogRow(node.Messages[messageIndex], level) + messageIndex++; + } + else + { + @Traverse(node.Children[childIndex], level + 1) + childIndex++; + } + } + if (messageIndex < node.Messages.Count) + { + for (var i = messageIndex; i < node.Messages.Count; i++) + { + @LogRow(node.Messages[i], level) + } + } + else + { + for (var i = childIndex; i < node.Children.Count; i++) + { + @Traverse(node.Children[i], level + 1) + } + } + // print end of scope + @LogRow(new LogInfo() + { + Name = node.Name, + Time = node.EndTime, + Severity = LogLevel.Verbose, + Message = string.Format("Completed {0} in {1}ms", node.State, node.EndTime - node.StartTime) + }, level); +} + + + + + + ASP.NET Logs + + + +

ASP.NET Logs

+
+ + + +
+ + + + + + + + + + + + + + + + + @foreach (var activity in Model.Activities.Reverse()) + { + + + @{ + if (activity.HttpInfo != null) + { + var requestPath = Model.Path.Value + "/" + activity.HttpInfo.RequestID; + + + + } + else + { + + } + } + + + + } +
PathHostStatus CodeLogs
@activity.HttpInfo.Path@activity.HttpInfo.Host@activity.HttpInfo.StatusCode@activity.Root.State + + + + + + + + + + + + + @if (activity.Id.Equals(Guid.Empty)) + { + // message not within a scope + @LogRow(activity.Root.Messages.FirstOrDefault(), 0) + } + else + { + @Traverse(activity.Root, 0) + } + +
DateTimeNameSeverityStateError
+
+ + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/Views/LogPage.css b/src/Microsoft.AspNet.Diagnostics.Elm/Views/LogPage.css new file mode 100644 index 00000000..ff0aa8ef --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/Views/LogPage.css @@ -0,0 +1,90 @@ +body { + font-size: .813em; + white-space: nowrap; + margin: 20px; +} + +col:nth-child(2) { + background-color: #FAFAFA; +} + +h1 { + margin-left: 25px; +} + +table { + margin: 0px auto; + border-collapse: collapse; + border-spacing: 0px; + table-layout: fixed; + width: 100%; +} + +td, th { + padding: 4px; +} + +thead { + font-size: 1em; + font-family: Arial; +} + +tr { + height: 23px; +} + +#requestHeader { + border-bottom: solid 1px gray; + border-top: solid 1px gray; + margin-bottom: 2px; + font-size: 1em; + line-height: 2em; +} + +.date, .time { + width: 70px; +} + +.logHeader { + border-bottom: 1px solid lightgray; + color: gray; + text-align: left; +} + +.logState { + text-overflow: ellipsis; + overflow: hidden; +} + +.logTd { + border-left: 1px solid gray; + padding: 0px; +} + +.logs { + width: 80%; +} + +.logRow:hover { + background-color: #D6F5FF; +} + +.requestRow>td { + border-bottom: solid 1px gray; +} + +.severity { + width: 80px; +} + +.tab { + margin-left: 30px; +} + +#viewOptions { + margin: 20px; +} + +#viewOptions > * { + margin: 5px; +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/Views/LogPageModel.cs b/src/Microsoft.AspNet.Diagnostics.Elm/Views/LogPageModel.cs new file mode 100644 index 00000000..9115cc16 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/Views/LogPageModel.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Diagnostics.Elm.Views +{ + public class LogPageModel + { + public IEnumerable Activities { get; set; } + + public ViewOptions Options { get; set; } + + public PathString Path { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/Views/RequestPage.cs b/src/Microsoft.AspNet.Diagnostics.Elm/Views/RequestPage.cs new file mode 100644 index 00000000..fbee9111 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/Views/RequestPage.cs @@ -0,0 +1,716 @@ +namespace Microsoft.AspNet.Diagnostics.Elm.Views +{ +#line 1 "RequestPage.cshtml" +using System + +#line default +#line hidden + ; +#line 2 "RequestPage.cshtml" +using System.Globalization + +#line default +#line hidden + ; +#line 3 "RequestPage.cshtml" +using System.Linq + +#line default +#line hidden + ; +#line 4 "RequestPage.cshtml" +using Microsoft.AspNet.Diagnostics.Elm + +#line default +#line hidden + ; +#line 5 "RequestPage.cshtml" +using Microsoft.AspNet.Diagnostics.Views + +#line default +#line hidden + ; +#line 6 "RequestPage.cshtml" +using Microsoft.AspNet.Diagnostics.Elm.Views + +#line default +#line hidden + ; +#line 7 "RequestPage.cshtml" +using Microsoft.Framework.Logging + +#line default +#line hidden + ; + using System.Threading.Tasks; + + public class RequestPage : Microsoft.AspNet.Diagnostics.Views.BaseView + { +public HelperResult +#line 19 "RequestPage.cshtml" +LogRow(LogInfo log) +{ + +#line default +#line hidden + return new HelperResult((__razor_helper_writer) => { +#line 20 "RequestPage.cshtml" + + if (log.Severity >= Model.Options.MinLevel && + (string.IsNullOrEmpty(Model.Options.NamePrefix) || log.Name.StartsWith(Model.Options.NamePrefix, StringComparison.Ordinal))) + { + +#line default +#line hidden + + WriteLiteralTo(__razor_helper_writer, " \r\n "); +#line 25 "RequestPage.cshtml" +WriteTo(__razor_helper_writer, string.Format("{0:MM/dd/yy}", log.Time)); + +#line default +#line hidden + WriteLiteralTo(__razor_helper_writer, "\r\n "); +#line 26 "RequestPage.cshtml" +WriteTo(__razor_helper_writer, string.Format("{0:H:mm:ss}", log.Time)); + +#line default +#line hidden + WriteLiteralTo(__razor_helper_writer, "\r\n (log.Severity.ToString().ToLowerInvariant(), 776), false)); + WriteLiteralTo(__razor_helper_writer, ">"); +#line 27 "RequestPage.cshtml" + WriteTo(__razor_helper_writer, log.Severity); + +#line default +#line hidden + WriteLiteralTo(__razor_helper_writer, "\r\n (log.Name, 864), false)); + WriteLiteralTo(__razor_helper_writer, ">"); +#line 28 "RequestPage.cshtml" + WriteTo(__razor_helper_writer, log.Name); + +#line default +#line hidden + WriteLiteralTo(__razor_helper_writer, "\r\n (log.Message, 914), false)); + WriteLiteralTo(__razor_helper_writer, " class=\"logState\" width=\"100px\">"); +#line 29 "RequestPage.cshtml" + WriteTo(__razor_helper_writer, log.Message); + +#line default +#line hidden + WriteLiteralTo(__razor_helper_writer, "\r\n (log.Exception, 1001), false)); + WriteLiteralTo(__razor_helper_writer, ">"); +#line 30 "RequestPage.cshtml" + WriteTo(__razor_helper_writer, log.Exception); + +#line default +#line hidden + WriteLiteralTo(__razor_helper_writer, "\r\n \r\n"); +#line 32 "RequestPage.cshtml" + } + +#line default +#line hidden + + } + ); +#line 33 "RequestPage.cshtml" +} + +#line default +#line hidden + +public HelperResult +#line 35 "RequestPage.cshtml" +Traverse(ScopeNode node) +{ + +#line default +#line hidden + return new HelperResult((__razor_helper_writer) => { +#line 36 "RequestPage.cshtml" + + var messageIndex = 0; + var childIndex = 0; + while (messageIndex < node.Messages.Count && childIndex < node.Children.Count) + { + if (node.Messages[messageIndex].Time < node.Children[childIndex].StartTime) + { + + +#line default +#line hidden + +#line 43 "RequestPage.cshtml" +WriteTo(__razor_helper_writer, LogRow(node.Messages[messageIndex])); + +#line default +#line hidden +#line 43 "RequestPage.cshtml" + + messageIndex++; + } + else + { + + +#line default +#line hidden + +#line 48 "RequestPage.cshtml" +WriteTo(__razor_helper_writer, Traverse(node.Children[childIndex])); + +#line default +#line hidden +#line 48 "RequestPage.cshtml" + + childIndex++; + } + } + if (messageIndex < node.Messages.Count) + { + for (var i = messageIndex; i < node.Messages.Count; i++) + { + + +#line default +#line hidden + +#line 56 "RequestPage.cshtml" +WriteTo(__razor_helper_writer, LogRow(node.Messages[i])); + +#line default +#line hidden +#line 56 "RequestPage.cshtml" + + } + } + else + { + for (var i = childIndex; i < node.Children.Count; i++) + { + + +#line default +#line hidden + +#line 63 "RequestPage.cshtml" +WriteTo(__razor_helper_writer, Traverse(node.Children[i])); + +#line default +#line hidden +#line 63 "RequestPage.cshtml" + + } + } + +#line default +#line hidden + + } + ); +#line 66 "RequestPage.cshtml" +} + +#line default +#line hidden + +#line 10 "RequestPage.cshtml" + + public RequestPage(RequestPageModel model) + { + Model = model; + } + + public RequestPageModel Model { get; set; } + +#line default +#line hidden + #line hidden + public RequestPage() + { + } + + #pragma warning disable 1998 + public override async Task ExecuteAsync() + { + WriteLiteral("\r\n"); + WriteLiteral("\r\n"); + WriteLiteral("\r\n"); + WriteLiteral(@" + + + + ASP.NET Logs + + + + +

ASP.NET Logs

+"); +#line 80 "RequestPage.cshtml" + + +#line default +#line hidden + +#line 80 "RequestPage.cshtml" + + var context = Model.Activity?.HttpInfo; + + +#line default +#line hidden + + WriteLiteral("\r\n"); +#line 83 "RequestPage.cshtml" + + +#line default +#line hidden + +#line 83 "RequestPage.cshtml" + if (context != null) + { + +#line default +#line hidden + + WriteLiteral("

Request Details

\r\n \r\n \r\n\r\n " + +" \r\n \r\n \r\n \r\n \r\n \r\n " + +" \r\n \r\n \r\n " + +"\r\n \r\n \r\n \r\n \r\n " + +" \r\n \r\n \r\n \r\n " + +" + + + + \r\n \r\n \r" + +"\n \r\n \r\n \r\n \r\n " + +" \r\n \r\n \r\n \r\n " + +" \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n " + +" \r\n \r\n \r\n \r\n " + +" \r\n \r\n
Path"); +#line 91 "RequestPage.cshtml" + Write(context.Path); + +#line default +#line hidden + WriteLiteral("
Host"); +#line 95 "RequestPage.cshtml" + Write(context.Host); + +#line default +#line hidden + WriteLiteral("
Content Type"); +#line 99 "RequestPage.cshtml" + Write(context.ContentType); + +#line default +#line hidden + WriteLiteral("
Method"); +#line 103 "RequestPage.cshtml" + Write(context.Method); + +#line default +#line hidden + WriteLiteral("
Protocol"); +#line 107 "RequestPage.cshtml" + Write(context.Protocol); + +#line default +#line hidden + WriteLiteral(@"
Headers + + + + + + + + +"); +#line 120 "RequestPage.cshtml" + + +#line default +#line hidden + +#line 120 "RequestPage.cshtml" + foreach (var header in context.Headers) + { + +#line default +#line hidden + + WriteLiteral(" \r\n \r\n \r\n \r\n"); +#line 126 "RequestPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral(" \r\n
VariableValue
"); +#line 123 "RequestPage.cshtml" + Write(header.Key); + +#line default +#line hidden + WriteLiteral(""); +#line 124 "RequestPage.cshtml" + Write(string.Join(";", header.Value)); + +#line default +#line hidden + WriteLiteral("
\r\n <" + +"/td>\r\n
Status Code"); +#line 133 "RequestPage.cshtml" + Write(context.StatusCode); + +#line default +#line hidden + WriteLiteral("
User"); +#line 137 "RequestPage.cshtml" + Write(context.User.Identity.Name); + +#line default +#line hidden + WriteLiteral("
Claims\r\n"); +#line 142 "RequestPage.cshtml" + + +#line default +#line hidden + +#line 142 "RequestPage.cshtml" + if (context.User.Claims.Any()) + { + +#line default +#line hidden + + WriteLiteral(@" + + + + + + + +"); +#line 152 "RequestPage.cshtml" + + +#line default +#line hidden + +#line 152 "RequestPage.cshtml" + foreach (var claim in context.User.Claims) + { + +#line default +#line hidden + + WriteLiteral(" \r\n " + +" \r\n \r\n \r\n"); +#line 158 "RequestPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral(" \r\n
IssuerValue
"); +#line 155 "RequestPage.cshtml" + Write(claim.Issuer); + +#line default +#line hidden + WriteLiteral(""); +#line 156 "RequestPage.cshtml" + Write(claim.Value); + +#line default +#line hidden + WriteLiteral("
\r\n"); +#line 161 "RequestPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral("
S" + +"cheme"); +#line 166 "RequestPage.cshtml" + Write(context.Scheme); + +#line default +#line hidden + WriteLiteral("
Query"); +#line 170 "RequestPage.cshtml" + Write(context.Query.Value); + +#line default +#line hidden + WriteLiteral("
Cookies\r\n"); +#line 175 "RequestPage.cshtml" + + +#line default +#line hidden + +#line 175 "RequestPage.cshtml" + if (context.Cookies.Any()) + { + +#line default +#line hidden + + WriteLiteral(@" + + + + + + + +"); +#line 185 "RequestPage.cshtml" + + +#line default +#line hidden + +#line 185 "RequestPage.cshtml" + foreach (var cookie in context.Cookies) + { + +#line default +#line hidden + + WriteLiteral(" \r\n " + +" \r\n \r\n \r\n"); +#line 191 "RequestPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral(" \r\n
VariableValue
"); +#line 188 "RequestPage.cshtml" + Write(cookie.Key); + +#line default +#line hidden + WriteLiteral(""); +#line 189 "RequestPage.cshtml" + Write(string.Join(";", cookie.Value)); + +#line default +#line hidden + WriteLiteral("
\r\n"); +#line 194 "RequestPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral("
\r\n"); +#line 198 "RequestPage.cshtml" + } + +#line default +#line hidden + + WriteLiteral("

Logs

\r\n
\r\n \r\n (Model.Options.NamePrefix, 6993), false)); + WriteLiteral(@" /> + +
+ + + + + + + + + + + + "); +#line 229 "RequestPage.cshtml" + Write(Traverse(Model.Activity.Root)); + +#line default +#line hidden + WriteLiteral(@" +
DateTimeSeverityNameStateError
+ + +"); + } + #pragma warning restore 1998 + } +} diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/Views/RequestPage.cshtml b/src/Microsoft.AspNet.Diagnostics.Elm/Views/RequestPage.cshtml new file mode 100644 index 00000000..60a4bf1b --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/Views/RequestPage.cshtml @@ -0,0 +1,239 @@ +@using System +@using System.Globalization +@using System.Linq +@using Microsoft.AspNet.Diagnostics.Elm +@using Microsoft.AspNet.Diagnostics.Views +@using Microsoft.AspNet.Diagnostics.Elm.Views +@using Microsoft.Framework.Logging + +@functions +{ + public RequestPage(RequestPageModel model) + { + Model = model; + } + + public RequestPageModel Model { get; set; } +} + +@helper LogRow(LogInfo log) +{ + if (log.Severity >= Model.Options.MinLevel && + (string.IsNullOrEmpty(Model.Options.NamePrefix) || log.Name.StartsWith(Model.Options.NamePrefix, StringComparison.Ordinal))) + { + + @string.Format("{0:MM/dd/yy}", log.Time) + @string.Format("{0:H:mm:ss}", log.Time) + @log.Severity + @log.Name + @log.Message + @log.Exception + + } +} + +@helper Traverse(ScopeNode node) +{ + var messageIndex = 0; + var childIndex = 0; + while (messageIndex < node.Messages.Count && childIndex < node.Children.Count) + { + if (node.Messages[messageIndex].Time < node.Children[childIndex].StartTime) + { + @LogRow(node.Messages[messageIndex]) + messageIndex++; + } + else + { + @Traverse(node.Children[childIndex]) + childIndex++; + } + } + if (messageIndex < node.Messages.Count) + { + for (var i = messageIndex; i < node.Messages.Count; i++) + { + @LogRow(node.Messages[i]) + } + } + else + { + for (var i = childIndex; i < node.Children.Count; i++) + { + @Traverse(node.Children[i]) + } + } +} + + + + + ASP.NET Logs + + + + +

ASP.NET Logs

+ @{ + var context = Model.Activity?.HttpInfo; + } + @if (context != null) + { +

Request Details

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Path@context.Path
Host@context.Host
Content Type@context.ContentType
Method@context.Method
Protocol@context.Protocol
Headers + + + + + + + + + @foreach (var header in context.Headers) + { + + + + + } + +
VariableValue
@header.Key@string.Join(";", header.Value)
+
Status Code@context.StatusCode
User@context.User.Identity.Name
Claims + @if (context.User.Claims.Any()) + { + + + + + + + + + @foreach (var claim in context.User.Claims) + { + + + + + } + +
IssuerValue
@claim.Issuer@claim.Value
+ } +
Scheme@context.Scheme
Query@context.Query.Value
Cookies + @if (context.Cookies.Any()) + { + + + + + + + + + @foreach (var cookie in context.Cookies) + { + + + + + } + +
VariableValue
@cookie.Key@string.Join(";", cookie.Value)
+ } +
+ } +

Logs

+
+ + + +
+ + + + + + + + + + + + @Traverse(Model.Activity.Root) +
DateTimeSeverityNameStateError
+ + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/Views/RequestPage.css b/src/Microsoft.AspNet.Diagnostics.Elm/Views/RequestPage.css new file mode 100644 index 00000000..d0803ebf --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/Views/RequestPage.css @@ -0,0 +1,56 @@ +body { + font-size: 0.9em; + width: 90%; + margin: 0px auto; +} + +h1 { + padding-bottom: 10px; +} + +h2 { + font-weight: normal; +} + +table { + border-spacing: 0px; + width: 100%; + border-collapse: collapse; + border: 1px solid black; + white-space: pre-wrap; +} + +th { + font-family: Arial; +} + +td, th { + padding: 8px; +} + +#headerTable, #cookieTable { + border: none; + height: 100%; +} + +#headerTd { + white-space: normal; +} + +#label { + width: 20%; + border-right: 1px solid black; +} + +#logs{ + margin-top: 10px; + margin-bottom: 20px; +} + +#logs>tbody>tr>td { + border-right: 1px dashed lightgray; +} + +#logs>thead>tr>th { + border: 1px solid black; +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/Views/RequestPageModel.cs b/src/Microsoft.AspNet.Diagnostics.Elm/Views/RequestPageModel.cs new file mode 100644 index 00000000..b42fa3cc --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/Views/RequestPageModel.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Diagnostics.Elm.Views +{ + public class RequestPageModel + { + public Guid RequestID { get; set; } + + public ActivityContext Activity { get; set; } + + public ViewOptions Options { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/Views/Shared.css b/src/Microsoft.AspNet.Diagnostics.Elm/Views/Shared.css new file mode 100644 index 00000000..7f355343 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/Views/Shared.css @@ -0,0 +1,39 @@ +body { + font-family: 'Segoe UI', Tahoma, Arial, Helvtica, sans-serif; + line-height: 1.4em; +} + +h1 { + font-family: 'Segoe UI', Helvetica, sans-serif; + font-size: 2.5em; +} + +td { + text-overflow: ellipsis; + overflow: hidden; +} + +tr:nth-child(2n) { + background-color: #F6F6F6; +} + +.critical { + background-color: red; + color: white; +} + +.error { + color: red; +} + +.information { + color: blue; +} + +.verbose { + color: black; +} + +.warning { + color: orange; +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics.Elm/project.json b/src/Microsoft.AspNet.Diagnostics.Elm/project.json new file mode 100644 index 00000000..5014719b --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics.Elm/project.json @@ -0,0 +1,29 @@ +{ + "version": "1.0.0-*", + "dependencies": { + "Microsoft.AspNet.Diagnostics": "1.0.0-*", + "Microsoft.AspNet.Http": "1.0.0-*", + "Microsoft.AspNet.Mvc.Razor": "6.0.0-*", + "Microsoft.AspNet.RequestContainer": "1.0.0-*", + "Microsoft.Framework.Logging.Interfaces": { "version": "1.0.0-*", "type": "build" }, + "Microsoft.Framework.Runtime.Interfaces": { "version": "1.0.0-*", "type": "build" } + }, + + "frameworks": { + "aspnet50": { }, + "aspnetcore50": { + "dependencies": { + "System.Collections.Concurrent": "4.0.0-beta-*", + "System.Collections": "4.0.10-beta-*", + "System.Diagnostics.TraceSource": "4.0.0-beta-*", + "System.Globalization": "4.0.10-beta-*", + "System.Runtime": "4.0.20-beta-*", + "System.Runtime.Extensions": "4.0.10-beta-*", + "System.Runtime.InteropServices": "4.0.20-beta-*", + "System.Linq": "4.0.0-beta-*", + "System.Threading": "4.0.0-beta-*", + "System.Threading.ExecutionContext": "4.0.0-beta-*" + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics/Views/BaseView.cs b/src/Microsoft.AspNet.Diagnostics/Views/BaseView.cs index ee2c49d4..f3f91794 100644 --- a/src/Microsoft.AspNet.Diagnostics/Views/BaseView.cs +++ b/src/Microsoft.AspNet.Diagnostics/Views/BaseView.cs @@ -100,6 +100,35 @@ protected void WriteAttribute( WriteLiteral(trailer.Item1); } + protected void WriteAttributeTo( + TextWriter writer, + string name, + Tuple leader, + Tuple trailer, + Tuple, Tuple, bool> part1) + { + if (name == null) + { + throw new ArgumentNullException("name"); + } + if (leader == null) + { + throw new ArgumentNullException("leader"); + } + if (trailer == null) + { + throw new ArgumentNullException("trailer"); + } + if (part1 == null) + { + throw new ArgumentNullException("part1"); + } + WriteLiteralTo(writer, leader.Item1); + WriteLiteralTo(writer, part1.Item1.Item1); + WriteTo(writer, part1.Item2.Item1); + WriteLiteralTo(writer, trailer.Item1); + } + /// /// /// @@ -421,7 +450,7 @@ protected void WriteAttribute( /// private void WriteEncoded(string value) { - Output.Write(WebUtility.HtmlEncode(value)); + WriteLiteralTo(Output, WebUtility.HtmlEncode(value)); } /// @@ -441,5 +470,72 @@ protected void Write(string value) { WriteEncoded(value); } + + /// + /// is invoked + /// + /// The to invoke + protected void Write(HelperResult result) + { + WriteTo(Output, result); + } + + /// + /// Writes the specified to . + /// + /// The instance to write to. + /// The to write. + /// + /// is invoked for types. + /// For all other types, the encoded result of is written to the + /// . + /// + protected void WriteTo(TextWriter writer, object value) + { + if (value != null) + { + var helperResult = value as HelperResult; + if (helperResult != null) + { + helperResult.WriteTo(writer); + } + else + { + WriteTo(writer, value.ToString()); + } + } + } + + /// + /// Writes the specified with HTML encoding to . + /// + /// The instance to write to. + /// The to write. + protected void WriteTo(TextWriter writer, string value) + { + WriteLiteralTo(writer, WebUtility.HtmlEncode(value)); + } + + /// + /// Writes the specified without HTML encoding to the . + /// + /// The instance to write to. + /// The to write. + protected void WriteLiteralTo(TextWriter writer, object value) + { + WriteLiteralTo(writer, Convert.ToString(value, CultureInfo.InvariantCulture)); + } + + /// + /// Writes the specified without HTML encoding to . + /// + /// The to write. + protected void WriteLiteralTo(TextWriter writer, string value) + { + if (!string.IsNullOrEmpty(value)) + { + writer.Write(value); + } + } } } diff --git a/src/Microsoft.AspNet.Diagnostics/Views/HelperResult.cs b/src/Microsoft.AspNet.Diagnostics/Views/HelperResult.cs new file mode 100644 index 00000000..e8f0e769 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics/Views/HelperResult.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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; + +namespace Microsoft.AspNet.Diagnostics.Views +{ + /// + /// Represents a deferred write operation in a . + /// + public class HelperResult + { + private readonly Action _action; + + /// + /// Creates a new instance of . + /// + /// The delegate to invoke when is called. + public HelperResult(Action action) + { + _action = action; + } + + /// + /// Gets the delegate to invoke when is called. + /// + public Action WriteAction + { + get { return _action; } + } + + /// + /// Method invoked to produce content from the . + /// + /// The instance to write to. + public virtual void WriteTo(TextWriter writer) + { + _action(writer); + } + } +} \ No newline at end of file diff --git a/src/PageGenerator/PageGenerator.kproj b/src/PageGenerator/PageGenerator.kproj index dc172378..5fc6e933 100644 --- a/src/PageGenerator/PageGenerator.kproj +++ b/src/PageGenerator/PageGenerator.kproj @@ -15,6 +15,10 @@ 2.0 + 0 + + + \ No newline at end of file diff --git a/src/PageGenerator/Program.cs b/src/PageGenerator/Program.cs index a4f84c78..11bd19b4 100644 --- a/src/PageGenerator/Program.cs +++ b/src/PageGenerator/Program.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using Microsoft.AspNet.Razor; +using Microsoft.AspNet.Razor.Generator; using Microsoft.Framework.Runtime; namespace PageGenerator @@ -76,6 +77,15 @@ private static void GenerateCodeFile(string cshtmlFilePath, string rootNamespace var codeLang = new CSharpRazorCodeLanguage(); var host = new RazorEngineHost(codeLang); host.DefaultBaseClass = "Microsoft.AspNet.Diagnostics.Views.BaseView"; + host.GeneratedClassContext = new GeneratedClassContext( + GeneratedClassContext.DefaultExecuteMethodName, + GeneratedClassContext.DefaultWriteMethodName, + GeneratedClassContext.DefaultWriteLiteralMethodName, + "WriteTo", + "WriteLiteralTo", + "HelperResult", + "DefineSection", + new GeneratedTagHelperContext()); var engine = new RazorTemplateEngine(host); using (var fileStream = File.OpenText(cshtmlFilePath)) diff --git a/src/PageGenerator/project.json b/src/PageGenerator/project.json index 228d7c87..177e5624 100644 --- a/src/PageGenerator/project.json +++ b/src/PageGenerator/project.json @@ -2,6 +2,7 @@ "version": "1.0.0-*", "dependencies": { "Microsoft.AspNet.Diagnostics": "1.0.0-*", + "Microsoft.AspNet.Diagnostics.Elm": "1.0.0-*", "Microsoft.AspNet.Razor": "4.0.0-*", "Microsoft.Framework.Runtime.Interfaces": "1.0.0-*" }, diff --git a/test/Microsoft.AspNet.Diagnostics.Tests/ElmLoggerTest.cs b/test/Microsoft.AspNet.Diagnostics.Tests/ElmLoggerTest.cs new file mode 100644 index 00000000..e4fa4a65 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Tests/ElmLoggerTest.cs @@ -0,0 +1,343 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Microsoft.AspNet.Diagnostics.Elm; +using Microsoft.Framework.Logging; +using Xunit; + +namespace Microsoft.AspNet.Diagnostics.Tests +{ + public class ElmLoggerTest + { + private const string _name = "test"; + private const string _state = "This is a test"; + private Func _filter = (_, __) => true; + + private Tuple SetUp(Func filter = null, string name = null) + { + // Arrange + var store = new ElmStore(); + var options = new ElmOptions() { Filter = filter ?? _filter }; + var provider = new ElmLoggerProvider(store, options); + var logger = (ElmLogger)provider.Create(name ?? _name); + + return new Tuple(logger, store); + } + + [Fact] + public void LogsWhenNullFormatterGiven() + { + // Arrange + var t = SetUp(); + var logger = t.Item1; + var store = t.Item2; + + // Act + logger.Write(LogLevel.Information, 0, _state, null, null); + + // Assert + Assert.Single(store.GetActivities()); + } + + [Fact] + public void DoesNotLogWithEmptyStateAndException() + { + // Arrange + var t = SetUp(); + var logger = t.Item1; + var store = t.Item2; + + // Act + logger.Write(LogLevel.Information, 0, null, null, null); + + // Assert + Assert.Empty(store.GetActivities()); + } + + [Fact] + public void DefaultLogsForAllLogLevels() + { + // Arrange + var t = SetUp(); + var logger = t.Item1; + var store = t.Item2; + + // Act + logger.Write(LogLevel.Verbose, 0, _state, null, null); + logger.Write(LogLevel.Information, 0, _state, null, null); + logger.Write(LogLevel.Warning, 0, _state, null, null); + logger.Write(LogLevel.Error, 0, _state, null, null); + logger.Write(LogLevel.Critical, 0, _state, null, null); + + // Assert + Assert.Equal(5, (store.GetActivities().SelectMany(a => NodeLogs(a.Root, new List()))).ToList().Count); + } + + [Theory] + [InlineData(LogLevel.Warning, "", 3)] + [InlineData(LogLevel.Warning, "te", 3)] + [InlineData(LogLevel.Warning, "bad", 0)] + [InlineData(LogLevel.Critical, "", 1)] + [InlineData(LogLevel.Critical, "test", 1)] + [InlineData(LogLevel.Verbose, "t", 5)] + public void Filter_LogsWhenAppropriate(LogLevel minLevel, string prefix, int count) + { + // Arrange + var t = SetUp((name, level) => (name.StartsWith(prefix, StringComparison.Ordinal) && level >= minLevel), _name); + var logger = t.Item1; + var store = t.Item2; + + // Act + logger.Write(LogLevel.Verbose, 0, _state, null, null); + logger.Write(LogLevel.Information, 0, _state, null, null); + logger.Write(LogLevel.Warning, 0, _state, null, null); + logger.Write(LogLevel.Error, 0, _state, null, null); + logger.Write(LogLevel.Critical, 0, _state, null, null); + + // Assert + Assert.Equal(count, (store.GetActivities().SelectMany(a => NodeLogs(a.Root, new List()))).ToList().Count); + } + + [Fact] + public void CountReturnsCorrectNumber() + { + // Arrange + var t = SetUp(); + var logger = t.Item1; + var store = t.Item2; + + // Act + using (logger.BeginScope("test14")) + { + for (var i = 0; i < 25; i++) + { + logger.WriteWarning("hello world"); + } + using (logger.BeginScope("test15")) + { + for (var i = 0; i < 25; i++) + { + logger.WriteCritical("goodbye world"); + } + } + } + + // Assert + Assert.Equal(50, store.Count()); + } + + [Fact] + public void ThreadsHaveSeparateActivityContexts() + { + // Arrange + var t = SetUp(); + var logger = t.Item1; + var store = t.Item2; + + var testThread = new TestThread(logger); + Thread workerThread = new Thread(testThread.work); + + // Act + workerThread.Start(); + using (logger.BeginScope("test1")) + { + logger.WriteWarning("hello world"); + Thread.Sleep(1000); + logger.WriteCritical("goodbye world"); + } + workerThread.Join(); + + // Assert + Assert.Equal(17, (store.GetActivities().SelectMany(a => NodeLogs(a.Root, new List()))).ToList().Count); + Assert.Equal(2, store.GetActivities().ToList().Count); + } + + [Fact] + public void ScopesHaveProperTreeStructure() + { + // Arrange + var t = SetUp(); + var logger = t.Item1; + var store = t.Item2; + + var testThread = new TestThread(logger); + Thread workerThread = new Thread(testThread.work); + + // Act + workerThread.Start(); + using (logger.BeginScope("test2")) + { + logger.WriteWarning("hello world"); + Thread.Sleep(1000); + logger.WriteCritical("goodbye world"); + } + workerThread.Join(); + + // Assert + // get the root of the activity for scope "test2" + var root1 = (store.GetActivities()).Where(a => a.Root.State.Equals("test2"))?.FirstOrDefault()?.Root; + Assert.NotNull(root1); + var root2 = (store.GetActivities()).Where(a => a.Root.State.Equals("test12"))?.FirstOrDefault()?.Root; + Assert.NotNull(root2); + + Assert.Equal(0, root1.Children.Count); + Assert.Equal(2, root1.Messages.Count); + Assert.Equal(1, root2.Children.Count); + Assert.Equal(12, root2.Messages.Count); + Assert.Equal(0, root2.Children.First().Children.Count); + Assert.Equal(3, root2.Children.First().Messages.Count); + } + + [Fact] + public void CollapseTree_CollapsesWhenNoLogsInSingleScope() + { + // Arrange + var t = SetUp(); + var logger = t.Item1; + var store = t.Item2; + + // Act + using (logger.BeginScope("test3")) + { + } + + // Assert + Assert.Empty(store.GetActivities()); + } + + [Fact] + public void CollapseTree_CollapsesWhenNoLogsInNestedScope() + { + // Arrange + var t = SetUp(); + var logger = t.Item1; + var store = t.Item2; + + // Act + using (logger.BeginScope("test4")) + { + using (logger.BeginScope("test5")) + { + } + } + + // Assert + Assert.Empty(store.GetActivities()); + } + + [Fact] + public void CollapseTree_DoesNotCollapseWhenLogsExist() + { + // Arrange + var t = SetUp(); + var logger = t.Item1; + var store = t.Item2; + + // Act + using (logger.BeginScope("test6")) + { + using (logger.BeginScope("test7")) + { + logger.WriteVerbose("hi"); + } + } + + // Assert + Assert.Single(store.GetActivities()); + } + + [Fact] + public void CollapseTree_CollapsesAppropriateNodes() + { + // Arrange + var t = SetUp(); + var logger = t.Item1; + var store = t.Item2; + + // Act + using (logger.BeginScope("test8")) + { + logger.WriteVerbose("hi"); + using (logger.BeginScope("test9")) + { + } + } + + // Assert + Assert.Single(store.GetActivities()); + var context = store.GetActivities().Where(a => a.Root.State.Equals("test8")).First(); + Assert.Empty(context.Root.Children); + } + + [Fact] + public void CollapseTree_WorksWithFilter() + { + // Arrange + var t = SetUp((_, level) => level >= LogLevel.Warning, null); + var logger = t.Item1; + var store = t.Item2; + + // Act + using (logger.BeginScope("test10")) + { + using (logger.BeginScope("test11")) + { + logger.WriteInformation("hi"); + } + } + + // Assert + Assert.Empty(store.GetActivities()); + } + + + private List NodeLogs(ScopeNode node, List logs) + { + if (node != null) + { + logs.AddRange(node.Messages); + foreach (var child in node.Children) + { + NodeLogs(child, logs); + } + } + return logs; + } + + private class TestThread + { + private ILogger _logger; + + public TestThread(ILogger logger) + { + _logger = logger; + } + + public void work() + { + using (_logger.BeginScope("test12")) + { + for (var i = 0; i < 5; i++) + { + _logger.WriteVerbose(string.Format("xxx {0}", i)); + Thread.Sleep(5); + } + using (_logger.BeginScope("test13")) + { + for (var i = 0; i < 3; i++) + { + _logger.WriteVerbose(string.Format("yyy {0}", i)); + Thread.Sleep(200); + } + } + for (var i = 0; i < 7; i++) + { + _logger.WriteVerbose(string.Format("zzz {0}", i)); + Thread.Sleep(40); + } + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Diagnostics.Tests/ElmMiddlewareTest.cs b/test/Microsoft.AspNet.Diagnostics.Tests/ElmMiddlewareTest.cs new file mode 100644 index 00000000..f6862ca7 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Tests/ElmMiddlewareTest.cs @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +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.Http; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +#if ASPNET50 +using Moq; +#endif +using Xunit; + +namespace Microsoft.AspNet.Diagnostics.Tests +{ + public class ElmMiddlewareTest + { + private const string DefaultPath = "/Elm"; + + [Fact] + public void DefaultPageOptions_HasDefaultPath() + { + // Arrange & act + var options = new ElmOptions(); + + // Assert + Assert.Equal(DefaultPath, options.Path.Value); + } + +#if ASPNET50 + [Fact] + public async void Invoke_WithNonMatchingPath_IgnoresRequest() + { + // Arrange + var elmStore = new ElmStore(); + var factory = new LoggerFactory(); + var optionsMock = new Mock>(); + optionsMock + .SetupGet(o => o.Options) + .Returns(new ElmOptions()); + factory.AddProvider(new ElmLoggerProvider(elmStore, optionsMock.Object.Options)); + + RequestDelegate next = _ => + { + return Task.FromResult(null); + }; + + var captureMiddleware = new ElmCaptureMiddleware( + next, + factory, + optionsMock.Object); + var pageMiddleware = new ElmPageMiddleware( + next, + optionsMock.Object, + elmStore); + + var contextMock = GetMockContext("/nonmatchingpath"); + + // Act + await captureMiddleware.Invoke(contextMock.Object); + await pageMiddleware.Invoke(contextMock.Object); + + // Assert + // Request.Query is used by the ElmPageMiddleware to parse the query parameters + contextMock.VerifyGet(c => c.Request.Query, Times.Never()); + } + + [Fact] + public async void Invoke_WithMatchingPath_FulfillsRequest() + { + // Arrange + var elmStore = new ElmStore(); + var factory = new LoggerFactory(); + var optionsMock = new Mock>(); + optionsMock + .SetupGet(o => o.Options) + .Returns(new ElmOptions()); + factory.AddProvider(new ElmLoggerProvider(elmStore, optionsMock.Object.Options)); + + RequestDelegate next = _ => + { + return Task.FromResult(null); + }; + + var captureMiddleware = new ElmCaptureMiddleware( + next, + factory, + optionsMock.Object); + var pageMiddleware = new ElmPageMiddleware( + next, + optionsMock.Object, + elmStore); + var contextMock = GetMockContext("/Elm"); + + using (var responseStream = new MemoryStream()) + { + contextMock + .SetupGet(c => c.Response.Body) + .Returns(responseStream); + + // Act + await captureMiddleware.Invoke(contextMock.Object); + await pageMiddleware.Invoke(contextMock.Object); + + string response = Encoding.UTF8.GetString(responseStream.ToArray()); + + // Assert + contextMock.VerifyGet(c => c.Request.Query, Times.AtLeastOnce()); + Assert.True(response.Contains("ASP.NET Logs")); + } + } + + [Fact] + public async void Invoke_BadRequestShowsError() + { + // Arrange + var elmStore = new ElmStore(); + var factory = new LoggerFactory(); + var optionsMock = new Mock>(); + optionsMock + .SetupGet(o => o.Options) + .Returns(new ElmOptions()); + factory.AddProvider(new ElmLoggerProvider(elmStore, optionsMock.Object.Options)); + + RequestDelegate next = _ => + { + return Task.FromResult(null); + }; + + var captureMiddleware = new ElmCaptureMiddleware( + next, + factory, + optionsMock.Object); + var pageMiddleware = new ElmPageMiddleware( + next, + optionsMock.Object, + elmStore); + var contextMock = GetMockContext("/Elm/666"); + + using (var responseStream = new MemoryStream()) + { + contextMock + .SetupGet(c => c.Response.Body) + .Returns(responseStream); + + // Act + await captureMiddleware.Invoke(contextMock.Object); + await pageMiddleware.Invoke(contextMock.Object); + + string response = Encoding.UTF8.GetString(responseStream.ToArray()); + + // Assert + contextMock.VerifyGet(c => c.Request.Query, Times.AtLeastOnce()); + Assert.True(response.Contains("Invalid Request Id")); + } + } + + private Mock GetMockContext(string path) + { + var contextMock = new Mock(MockBehavior.Strict); + contextMock + .SetupGet(c => c.Request.Path) + .Returns(new PathString(path)); + contextMock + .SetupGet(c => c.Request.Host) + .Returns(new HostString("localhost")); + contextMock + .SetupGet(c => c.Request.ContentType) + .Returns(""); + contextMock + .SetupGet(c => c.Request.Scheme) + .Returns("http"); + contextMock + .SetupGet(c => c.Request.Scheme) + .Returns("http"); + contextMock + .SetupGet(c => c.Response.StatusCode) + .Returns(200); + contextMock + .SetupGet(c => c.Response.Body) + .Returns(new Mock().Object); + contextMock + .SetupGet(c => c.User) + .Returns(new ClaimsPrincipal()); + contextMock + .SetupGet(c => c.Request.Method) + .Returns("GET"); + contextMock + .SetupGet(c => c.Request.Protocol) + .Returns("HTTP/1.1"); + contextMock + .SetupGet(c => c.Request.Headers) + .Returns(new Mock().Object); + contextMock + .SetupGet(c => c.Request.QueryString) + .Returns(new QueryString()); + contextMock + .SetupGet(c => c.Request.Query) + .Returns(new Mock().Object); + contextMock + .SetupGet(c => c.Request.Cookies) + .Returns(new Mock().Object); + + return contextMock; + } +#endif + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Diagnostics.Tests/project.json b/test/Microsoft.AspNet.Diagnostics.Tests/project.json index b5cdf1db..928eb2c1 100644 --- a/test/Microsoft.AspNet.Diagnostics.Tests/project.json +++ b/test/Microsoft.AspNet.Diagnostics.Tests/project.json @@ -3,7 +3,8 @@ "warningsAsErrors": true }, "dependencies": { - "Microsoft.AspNet.Diagnostics": "", + "Microsoft.AspNet.Diagnostics": "1.0.0-*", + "Microsoft.AspNet.Diagnostics.Elm": "1.0.0-*", "Xunit.KRunner": "1.0.0-*" },