diff --git a/src/Shared/LayoutRenderers/AspNetRequestHeadersLayoutRenderer.cs b/src/Shared/LayoutRenderers/AspNetRequestHeadersLayoutRenderer.cs
new file mode 100644
index 00000000..b452adc2
--- /dev/null
+++ b/src/Shared/LayoutRenderers/AspNetRequestHeadersLayoutRenderer.cs
@@ -0,0 +1,115 @@
+using System.Collections.Generic;
+using System.Text;
+using NLog.Config;
+using NLog.LayoutRenderers;
+using NLog.Web.Internal;
+using System;
+#if !ASP_NET_CORE
+using System.Collections.Specialized;
+using System.Linq;
+#else
+using Microsoft.AspNetCore.Http;
+#endif
+
+namespace NLog.Web.LayoutRenderers
+{
+ ///
+ /// ASP.NET Request Headers
+ ///
+ /// Example usage of ${aspnet-request-headers}
+ ///
+ ///
+ /// ${aspnet-request-headers:OutputFormat=Flat}
+ /// ${aspnet-request-headers:OutputFormat=Json}
+ /// ${aspnet-request-headers:OutputFormat=Json:HeaderNames=username}
+ /// ${aspnet-request-headers:OutputFormat=Json:Exclude=access_token}
+ ///
+ ///
+ [LayoutRenderer("aspnet-request-headers")]
+ [ThreadSafe]
+ public class AspNetRequestHeadersLayoutRenderer : AspNetLayoutMultiValueRendererBase
+ {
+ ///
+ /// Header names to be rendered.
+ /// If null or empty array, all headers will be rendered.
+ ///
+ public List HeaderNames { get; set; }
+
+ ///
+ /// Gets or sets the keys to exclude from the output. If omitted, none are excluded.
+ ///
+ ///
+#if ASP_NET_CORE
+ public ISet Exclude { get; set; }
+#else
+ public HashSet Exclude { get; set; }
+#endif
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AspNetRequestHeadersLayoutRenderer()
+ {
+ Exclude = new HashSet(new[] { "ALL_HTTP", "ALL_RAW", "AUTH_PASSWORD" }, StringComparer.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Renders the ASP.NET Headers appends it to the specified .
+ ///
+ /// The to append the rendered data to.
+ /// Logging event.
+ protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent)
+ {
+ var httpRequest = HttpContextAccessor.HttpContext.TryGetRequest();
+ if (httpRequest == null)
+ {
+ return;
+ }
+
+ var headers = httpRequest.Headers;
+ if (headers?.Count > 0)
+ {
+ bool checkForExclude = (HeaderNames == null || HeaderNames.Count == 0) && Exclude?.Count > 0;
+ var headerValues = GetHeaderValues(headers, checkForExclude);
+ SerializePairs(headerValues, builder, logEvent);
+ }
+ }
+
+#if !ASP_NET_CORE
+ private IEnumerable> GetHeaderValues(NameValueCollection headers, bool checkForExclude)
+ {
+ var headerNames = HeaderNames?.Count > 0 ? HeaderNames : headers.Keys.Cast().ToList();
+ foreach (var headerName in headerNames)
+ {
+ if (checkForExclude && Exclude.Contains(headerName))
+ continue;
+
+ var headerValue = headers[headerName];
+ if (headerValue == null)
+ {
+ continue;
+ }
+
+ yield return new KeyValuePair(headerName, headerValue);
+ }
+ }
+#else
+ private IEnumerable> GetHeaderValues(IHeaderDictionary headers, bool checkForExclude)
+ {
+ var headerNames = HeaderNames?.Count > 0 ? HeaderNames : headers.Keys;
+ foreach (var headerName in headerNames)
+ {
+ if (checkForExclude && Exclude.Contains(headerName))
+ continue;
+
+ if (!headers.TryGetValue(headerName, out var headerValue))
+ {
+ continue;
+ }
+
+ yield return new KeyValuePair(headerName, headerValue);
+ }
+ }
+#endif
+ }
+}
\ No newline at end of file
diff --git a/tests/Shared/LayoutRenderers/AspNetRequestHeadersLayoutRendererTests.cs b/tests/Shared/LayoutRenderers/AspNetRequestHeadersLayoutRendererTests.cs
new file mode 100644
index 00000000..e278e651
--- /dev/null
+++ b/tests/Shared/LayoutRenderers/AspNetRequestHeadersLayoutRendererTests.cs
@@ -0,0 +1,335 @@
+using System;
+using System.Collections.Generic;
+using NLog.Web.LayoutRenderers;
+using NLog.Web.Enums;
+using Xunit;
+using System.Collections.Specialized;
+
+#if !ASP_NET_CORE
+using NSubstitute;
+using System.Web;
+#else
+using Microsoft.Extensions.Primitives;
+#endif
+
+namespace NLog.Web.Tests.LayoutRenderers
+{
+ public class AspNetRequestHeadersLayoutRendererTests : TestInvolvingAspNetHttpContext
+ {
+ [Fact]
+ public void NullKeyRendersAllHeaders()
+ {
+#if ASP_NET_CORE
+ var expectedResult = "Host=stackoverflow.com,key=TEST,Key1=TEST1"; // ASP.NET Core automatically includes host of request URL as part of headers
+#else
+ var expectedResult = "key=TEST,Key1=TEST1";
+#endif
+ var renderer = CreateRenderer();
+ renderer.HeaderNames = null;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void NullKeyRendersAllHeadersExceptExcluded()
+ {
+#if ASP_NET_CORE
+ var expectedResult = "Host=stackoverflow.com,Key1=TEST1"; // ASP.NET Core automatically includes host of request URL as part of headers
+#else
+ var expectedResult = "Key1=TEST1";
+#endif
+ var renderer = CreateRenderer();
+ renderer.HeaderNames = null;
+ renderer.Exclude.Add("key");
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyNotFoundRendersEmptyString_Flat_Formatting()
+ {
+ var renderer = CreateRenderer();
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Flat;
+ renderer.HeaderNames = new List { "notfound" };
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void KeyNotFoundRendersEmptyString_Json_Formatting()
+ {
+ var renderer = CreateRenderer();
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Json;
+ renderer.HeaderNames = new List { "notfound" };
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Multiple_Headers_Flat_Formatting()
+ {
+ var expectedResult = "key=TEST,Key1=TEST1";
+
+ var renderer = CreateRenderer();
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Multiple_Headers_Flat_Formatting_separators()
+ {
+ var expectedResult = "key:TEST|Key1:TEST1";
+
+ var renderer = CreateRenderer();
+ renderer.ValueSeparator = ":";
+ renderer.ItemSeparator = "|";
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Multiple_Headers_Flat_Formatting_separators_layouts()
+ {
+ var expectedResult = "key>TEST" + Environment.NewLine + "Key1>TEST1";
+
+ var renderer = CreateRenderer();
+ renderer.ValueSeparator = "${event-properties:valueSeparator1}";
+ renderer.ItemSeparator = "${newline}";
+
+ var logEventInfo = new LogEventInfo();
+ logEventInfo.Properties["valueSeparator1"] = ">";
+
+ // Act
+ string result = renderer.Render(logEventInfo);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Single_Header_Flat_Formatting()
+ {
+ var expectedResult = "key=TEST";
+
+ var renderer = CreateRenderer(addSecondHeader: false);
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Single_Header_Json_Formatting()
+ {
+ var expectedResult = "[{\"key\":\"TEST\"}]";
+
+ var renderer = CreateRenderer(addSecondHeader: false);
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Json;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Single_Header_Json_Formatting_no_array()
+ {
+ var expectedResult = "{\"key\":\"TEST\"}";
+
+ var renderer = CreateRenderer(addSecondHeader: false);
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Json;
+ renderer.SingleAsArray = false;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void KeyFoundRendersValue_Multiple_Headers_Json_Formatting(bool singleAsArray)
+ {
+ var expectedResult = "[{\"key\":\"TEST\"},{\"Key1\":\"TEST1\"}]";
+
+ var renderer = CreateRenderer();
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Json;
+ renderer.SingleAsArray = singleAsArray;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyNotFoundRendersEmptyString_Flat_Formatting_ValuesOnly()
+ {
+ var renderer = CreateRenderer();
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Flat;
+ renderer.HeaderNames = new List { "notfound" };
+ renderer.ValuesOnly = true;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void KeyNotFoundRendersEmptyString_Json_Formatting_ValuesOnly()
+ {
+ var renderer = CreateRenderer();
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Json;
+ renderer.HeaderNames = new List { "notfound" };
+ renderer.ValuesOnly = true;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Header_Multiple_Items_Flat_Formatting_ValuesOnly()
+ {
+ var expectedResult = "TEST,TEST1";
+
+ var renderer = CreateRenderer();
+ renderer.ValuesOnly = true;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Header_Multiple_Items_Flat_Formatting_separators_ValuesOnly()
+ {
+ var expectedResult = "TEST|TEST1";
+
+ var renderer = CreateRenderer();
+ renderer.ValueSeparator = ":";
+ renderer.ItemSeparator = "|";
+ renderer.ValuesOnly = true;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Single_Item_Flat_Formatting_ValuesOnly()
+ {
+ var expectedResult = "TEST";
+
+ var renderer = CreateRenderer(addSecondHeader: false);
+ renderer.ValuesOnly = true;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Single_Item_Json_Formatting_ValuesOnly()
+ {
+ var expectedResult = "[\"TEST\"]";
+
+ var renderer = CreateRenderer(addSecondHeader: false);
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Json;
+ renderer.ValuesOnly = true;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Single_Item_Json_Formatting_no_array_ValuesOnly()
+ {
+ // With ValuesOnly enabled, only arrays are valid
+ var expectedResult = "[\"TEST\"]";
+
+ var renderer = CreateRenderer(addSecondHeader: false);
+
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Json;
+ renderer.SingleAsArray = false;
+ renderer.ValuesOnly = true;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void KeyFoundRendersValue_Header_Multiple_Items_Json_Formatting_ValuesOnly(bool singleAsArray)
+ {
+ var expectedResult = "[\"TEST\",\"TEST1\"]";
+
+ var renderer = CreateRenderer();
+
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Json;
+ renderer.SingleAsArray = singleAsArray;
+ renderer.ValuesOnly = true;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ ///
+ /// Create headers renderer with mocked HTTP context
+ ///
+ /// Add second header
+ /// Created headers layout renderer
+ private AspNetRequestHeadersLayoutRenderer CreateRenderer(bool addSecondHeader = true)
+ {
+ var headerNames = new List();
+#if ASP_NET_CORE
+ var httpContext = HttpContext;
+#else
+ var httpContext = Substitute.For();
+#endif
+
+#if ASP_NET_CORE
+ headerNames.Add("key");
+ httpContext.Request.Headers.Add("key", new StringValues("TEST"));
+
+ if (addSecondHeader)
+ {
+ headerNames.Add("Key1");
+ httpContext.Request.Headers.Add("Key1", new StringValues("TEST1"));
+ }
+#else
+ var headers = new NameValueCollection();
+ headers.Add("key", "TEST");
+ headerNames.Add("key");
+
+ if (addSecondHeader)
+ {
+ headers.Add("Key1", "TEST1");
+ headerNames.Add("Key1");
+ }
+
+ httpContext.Request.Headers.Returns(headers);
+#endif
+
+ var renderer = new AspNetRequestHeadersLayoutRenderer();
+ renderer.HttpContextAccessor = new FakeHttpContextAccessor(httpContext);
+ renderer.HeaderNames = headerNames;
+ return renderer;
+ }
+ }
+}