From 4083f964dc4deb26b0960f96840e5e35fe4179bb Mon Sep 17 00:00:00 2001 From: Jimmy Campbell Date: Wed, 22 Feb 2017 12:31:19 -0800 Subject: [PATCH] Added request tracing traces API. --- .../Controllers/RequestTracesController.cs | 76 ++++++++ .../Defines.cs | 7 + .../Helpers/TracesHelper.cs | 183 ++++++++++++++++++ .../Startup.cs | 10 + .../TraceId.cs | 41 ++++ .../TraceInfo.cs | 21 ++ 6 files changed, 338 insertions(+) create mode 100644 src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/Controllers/RequestTracesController.cs create mode 100644 src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/Helpers/TracesHelper.cs create mode 100644 src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/TraceId.cs create mode 100644 src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/TraceInfo.cs diff --git a/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/Controllers/RequestTracesController.cs b/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/Controllers/RequestTracesController.cs new file mode 100644 index 00000000..3e46bda7 --- /dev/null +++ b/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/Controllers/RequestTracesController.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + + +namespace Microsoft.IIS.Administration.WebServer.HttpRequestTracing +{ + using AspNetCore.Mvc; + using Core; + using Core.Http; + using Files; + using Sites; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Web.Administration; + + public class RequestTracesController : ApiBaseController + { + private IFileProvider _provider; + + public RequestTracesController(IFileProvider provider) + { + _provider = provider; + } + + [HttpGet] + [ResourceInfo(Name = Defines.TracesName)] + public async Task Get() + { + string hrtUuid = Context.Request.Query[Defines.IDENTIFIER]; + + if (string.IsNullOrEmpty(hrtUuid)) { + return NotFound(); + } + + var id = new HttpRequestTracingId(hrtUuid); + Site site = id?.SiteId == null ? null : SiteHelper.GetSite(id.SiteId.Value); + + if (site == null) { + return NotFound(); + } + + var helper = new TracesHelper(_provider, site); + + IEnumerable traces = await helper.GetTraces(); + + Context.Response.SetItemsCount(traces.Count()); + + return new { + traces = traces.Select(t => helper.ToJsonModel(t, Context.Request.GetFields())) + }; + } + + [HttpGet] + [ResourceInfo(Name = Defines.TraceName)] + public async Task Get(string id) + { + TraceId traceId = new TraceId(id); + Site site = SiteHelper.GetSite(traceId.SiteId); + + if (site == null) { + return NotFound(); + } + + var helper = new TracesHelper(_provider, site); + + TraceInfo trace = await helper.GetTrace(traceId.Name); + + if (trace == null) { + return NotFound(); + } + + return helper.ToJsonModel(trace, Context.Request.GetFields()); + } + } +} diff --git a/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/Defines.cs b/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/Defines.cs index 0e2a9210..a5e65638 100644 --- a/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/Defines.cs +++ b/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/Defines.cs @@ -12,6 +12,7 @@ public class Defines private const string ENDPOINT = "http-request-tracing"; private const string PROVIDERS_ENDPOINT = "providers"; private const string RULES_ENDPOINT = "rules"; + private const string TRACES_ENDPOINT = "traces"; public const string HttpRequestTracingName = "Microsoft.WebServer.HttpRequestTracing"; public static readonly string PATH = $"{WebServer.Defines.PATH}/{ENDPOINT}"; @@ -29,5 +30,11 @@ public class Defines public static readonly string RULES_PATH = $"{PATH}/{RULES_ENDPOINT}"; public static readonly ResDef RulesResource = new ResDef("rules", new Guid("FE6059CD-DF4D-47E9-AAB3-5C380874E323"), RULES_ENDPOINT); public const string RULES_IDENTIFIER = "rule.id"; + + public const string TracesName = "Microsoft.WebServer.HttpRequestTracing.Traces"; + public const string TraceName = "Microsoft.WebServer.HttpRequestTracing.Trace"; + public static readonly string TRACES_PATH = $"{PATH}/{TRACES_ENDPOINT}"; + public static readonly ResDef TracesResource = new ResDef("traces", new Guid("3A103124-E4AF-49D9-A379-949F314A89F1"), TRACES_ENDPOINT); + public const string TRACES_IDENTIFIER = "trace.id"; } } diff --git a/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/Helpers/TracesHelper.cs b/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/Helpers/TracesHelper.cs new file mode 100644 index 00000000..8f6e2f86 --- /dev/null +++ b/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/Helpers/TracesHelper.cs @@ -0,0 +1,183 @@ +namespace Microsoft.IIS.Administration.WebServer.HttpRequestTracing +{ + using Core; + using Core.Utils; + using Files; + using System; + using System.Collections.Generic; + using System.Dynamic; + using System.IO; + using System.Linq; + using System.Threading.Tasks; + using System.Xml; + using Web.Administration; + + sealed class TracesHelper + { + private static readonly Fields RefFields = new Fields("url", "id", "http_status", "method", "time_taken"); + + private static readonly XmlReaderSettings _xmlReaderSettings = new XmlReaderSettings() { + Async = true + }; + + private IFileProvider _provider; + private Site _site; + + public TracesHelper(IFileProvider provider, Site site) + { + _provider = provider; + _site = site; + } + + public async Task> GetTraces() + { + IEnumerable files = null; + string dir = _site.TraceFailedRequestsLogging.Directory; + string path = string.IsNullOrEmpty(dir) ? null : Path.Combine(PathUtil.GetFullPath(dir), "W3SVC" + _site.Id); + + if (path != null) { + files = _provider.GetFiles(path, "*.xml"); + } + + return await Task.WhenAll(files.Select(f => GetTraceInternal(f))); + } + + public async Task GetTrace(string id) + { + return (await GetTraces()).FirstOrDefault(t => t.File.Name.Equals(id, StringComparison.OrdinalIgnoreCase)); + } + + public object ToJsonModel(TraceInfo trace, Fields fields = null, bool full = true) + { + TraceId traceId = new TraceId(_site.Id, trace.File.Name); + + if (fields == null) { + fields = Fields.All; + } + + dynamic obj = new ExpandoObject(); + + // + // id + obj.id = traceId.Uuid; + + // + // url + if (fields.Exists("url") && !string.IsNullOrEmpty(trace.Url)) { + obj.url = trace.Url; + } + + // + // method + if (fields.Exists("method") && !string.IsNullOrEmpty(trace.Method)) { + obj.method = trace.Method; + } + + // + // status_code + if (fields.Exists("status_code") && trace.StatusCode > 0) { + obj.status_code = trace.StatusCode; + } + + // + // date + if (fields.Exists("date")) { + obj.date = trace.Date; + } + + // + // time_taken + if (fields.Exists("time_taken")) { + obj.time_taken = trace.TimeTaken; + } + + // + // process_id + if (fields.Exists("process_id") && !string.IsNullOrEmpty(trace.ProcessId)) { + obj.process_id = trace.ProcessId; + } + + // + // activity_id + if (fields.Exists("activity_id") && !string.IsNullOrEmpty(trace.ActivityId)) { + obj.activity_id = trace.ActivityId; + } + + // + // file_info + if (fields.Exists("file_info")) { + obj.file_info = new FilesHelper(_provider).ToJsonModelRef(trace.File, fields.Filter("file_info")); + } + + // + // request_tracing + if (fields.Exists("request_tracing")) { + obj.request_tracing = Helper.ToJsonModelRef(_site, "/"); + } + + return Core.Environment.Hal.Apply(Defines.TracesResource.Guid, obj, full); ; + } + + public object ToJsonModelRef(TraceInfo trace, Fields fields = null) + { + if (fields == null || !fields.HasFields) { + return ToJsonModel(trace, RefFields, false); + } + else { + return ToJsonModel(trace, fields, false); + } + } + + public static string GetLocation(string id) + { + return $"/{Defines.TRACES_PATH}/{id}"; + } + + private Task GetTraceInternal(IFileInfo trace) + { + return ParseTrace(trace); + } + + private async Task ParseTrace(IFileInfo trace) + { + TraceInfo info = null; + + + using (var stream = _provider.GetFileStream(trace.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + using (var reader = XmlReader.Create(stream, _xmlReaderSettings)) { + try { + + while (await reader.ReadAsync()) { + if (reader.NodeType == XmlNodeType.Element && reader.Name.Equals("failedRequest")) { + + info = new TraceInfo() { + File = trace, + Url = reader.GetAttribute("url"), + Method = reader.GetAttribute("verb"), + Date = trace.Created, + ProcessId = reader.GetAttribute("processId"), + ActivityId = reader.GetAttribute("activityId") + }; + + float.TryParse(reader.GetAttribute("triggerStatusCode"), out info.StatusCode); + int.TryParse(reader.GetAttribute("timeTaken"), out info.TimeTaken); + + break; + } + } + } + catch(XmlException) { + // Ignore malformatted XML + } + } + + if (info == null) { + info = new TraceInfo() { + File = trace + }; + } + + return info; + } + } +} diff --git a/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/Startup.cs b/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/Startup.cs index 547734b0..bd38d356 100644 --- a/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/Startup.cs +++ b/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/Startup.cs @@ -21,6 +21,7 @@ public override void Start() ConfigureHttpRequestTracing(); ConfigureProviders(); ConfigureRules(); + ConfigureTraces(); } private void ConfigureXsl() @@ -84,5 +85,14 @@ private void ConfigureRules() Environment.Hal.ProvideLink(Defines.Resource.Guid, Defines.RulesResource.Name, hrt => new { href = $"/{Defines.RULES_PATH}?{Defines.IDENTIFIER}={hrt.id}" }); } + + private void ConfigureTraces() + { + Environment.Host.RouteBuilder.MapWebApiRoute(Defines.TracesResource.Guid, $"{ Defines.TRACES_PATH}/{{id?}}", new { controller = "RequestTraces" }); + + Environment.Hal.ProvideLink(Defines.TracesResource.Guid, "self", r => new { href = TracesHelper.GetLocation(r.id) }); + + Environment.Hal.ProvideLink(Defines.Resource.Guid, Defines.TracesResource.Name, hrt => new { href = $"/{Defines.TRACES_PATH}?{Defines.IDENTIFIER}={hrt.id}" }); + } } } diff --git a/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/TraceId.cs b/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/TraceId.cs new file mode 100644 index 00000000..cdf9c156 --- /dev/null +++ b/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/TraceId.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + + +namespace Microsoft.IIS.Administration.WebServer.HttpRequestTracing +{ + using System; + + public class TraceId + { + private const string PURPOSE = "WebServer.HttpRequestTracing.Trace"; + private const char DELIMITER = '\n'; + + private const uint SITE_ID_INDEX = 0; + private const uint NAME_INDEX = 1; + + public long SiteId { get; private set; } + public string Name { get; private set; } + public string Uuid { get; private set; } + + public TraceId(string uuid) + { + if (string.IsNullOrEmpty(uuid)) { + throw new ArgumentNullException("uuid"); + } + + var info = Core.Utils.Uuid.Decode(uuid, PURPOSE).Split(DELIMITER); + + this.Name = info[NAME_INDEX]; + this.SiteId = long.Parse(info[SITE_ID_INDEX]); + this.Uuid = uuid; + } + + public TraceId(long siteId, string name) + { + this.SiteId = siteId; + this.Name = name; + this.Uuid = Core.Utils.Uuid.Encode(this.Name + DELIMITER + this.SiteId.ToString(), PURPOSE); + } + } +} diff --git a/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/TraceInfo.cs b/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/TraceInfo.cs new file mode 100644 index 00000000..357bc496 --- /dev/null +++ b/src/Microsoft.IIS.Administration.WebServer.HttpRequestTracing/TraceInfo.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + + +namespace Microsoft.IIS.Administration.WebServer.HttpRequestTracing +{ + using Files; + using System; + + class TraceInfo + { + public IFileInfo File; + public string Url; + public string Method; + public float StatusCode; + public DateTime Date; + public int TimeTaken; + public string ProcessId; + public string ActivityId; + } +}