Skip to content

Commit

Permalink
Allow disabling IP masking (#14)
Browse files Browse the repository at this point in the history
* Added telemetry initializer adding client IP to custom dimensions
* Made client IP key configurable
  • Loading branch information
matthiasguentert authored Oct 13, 2022
1 parent 9f83528 commit 96993b1
Show file tree
Hide file tree
Showing 15 changed files with 230 additions and 35 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,4 @@ MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
/.idea/
/test/ManualTests/appsettings.Development.json
7 changes: 7 additions & 0 deletions ApplicationInsightsRequestLogging.sln
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{CD74FC7F-0
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApplicationInsightsRequestLoggingTests", "test\ApplicationInsightsRequestLoggingTests\ApplicationInsightsRequestLoggingTests.csproj", "{0CB48D95-C03F-4BD1-A4C0-DD1F7CC73F56}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManualTests", "test\ManualTests\ManualTests.csproj", "{B711C0B7-E8FF-48E9-8A93-3A5DB113088E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -34,6 +36,10 @@ Global
{0CB48D95-C03F-4BD1-A4C0-DD1F7CC73F56}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0CB48D95-C03F-4BD1-A4C0-DD1F7CC73F56}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0CB48D95-C03F-4BD1-A4C0-DD1F7CC73F56}.Release|Any CPU.Build.0 = Release|Any CPU
{B711C0B7-E8FF-48E9-8A93-3A5DB113088E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B711C0B7-E8FF-48E9-8A93-3A5DB113088E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B711C0B7-E8FF-48E9-8A93-3A5DB113088E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B711C0B7-E8FF-48E9-8A93-3A5DB113088E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -44,5 +50,6 @@ Global
GlobalSection(NestedProjects) = preSolution
{F88928B2-6AEA-485E-9AEE-0E0B3752302E} = {B1127F87-7877-4664-8E3E-9F86202A51DE}
{0CB48D95-C03F-4BD1-A4C0-DD1F7CC73F56} = {CD74FC7F-0463-4FC1-8735-C9F430477CE8}
{B711C0B7-E8FF-48E9-8A93-3A5DB113088E} = {CD74FC7F-0463-4FC1-8735-C9F430477CE8}
EndGlobalSection
EndGlobal
33 changes: 24 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Extended HTTP request & response logging with Application Insights
# Extended request logging with Application Insights

## Introduction

This nuget package provides a custom middleware that allows to write the body of an HTTP request/response to a custom dimension.

![](https://i.imgur.com/0fxsnKN.png)
![](https://i.imgur.com/CNbVKsx.png)

## Features

Expand All @@ -13,7 +13,8 @@ This nuget package provides a custom middleware that allows to write the body of
- Configure HTTP status code ranges that will trigger logging
- Configure maximum body length to store
- Provide optional cut off text
- Configure name of custom dimension key
- Configure name of custom dimension keys
- Disable IP masking without the need to modify the App Insights resource as described [here](https://learn.microsoft.com/en-us/azure/azure-monitor/app/ip-collection?tabs=net)

> A word of warning! Writing the content of an HTTP body to Application Insights might reveal sensitive user information that otherwise would be hidden and protected in transfer via TLS. So use this with care and only during debugging or developing!
Expand All @@ -36,14 +37,17 @@ public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddApplicationInsightsTelemetry(Configuration["APPINSIGHTS_CONNECTIONSTRING"]);
// Register App Insights
services.AddApplicationInsightsTelemetry();

// Register this middleware
services.AddAppInsightsHttpBodyLogging();

// ...
}
```

Finally configure the request pipeline.
Finally configure the request pipeline. Make sure the call to `UseAppInsightsHttpBodyLogging` happens as early as possible as the [order matters](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-6.0#middleware-order). Have a look at this [issue](https://github.com/matthiasguentert/azure-appinsights-logger/issues/11)

```csharp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
Expand All @@ -59,18 +63,19 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
```
## Configuration

You can overwrite the default settings as follows:
You can overwrite the default settings as follows...

```csharp
services.AddAppInsightsHttpBodyLogging(o => {
o.HttpCodes.Add(StatusCodes.Status200OK);
o.HttpVerbs.Add(HttpMethods.Get);
o.MaxBytes = 10000
o.Appendix = "\nSNIP"
o.MaxBytes = 10000;
o.Appendix = "\nSNIP";
o.DisableIpMasking = true;
});
```

Or stick with the defaults which are defined in `BodyLoggerOptions`.
...or stick with the defaults which are defined in `BodyLoggerOptions`.

### BodyLoggerOptions

Expand Down Expand Up @@ -108,6 +113,11 @@ public class BodyLoggerOptions
/// </summary>
public string ResponseBodyPropertyKey { get; set; } = "ResponseBody";

/// <summary>
/// Which property key should be used
/// </summary>
public string ClientIpPropertyKey { get; set; } = "ClientIp";

/// <summary>
/// Defines the amount of bytes that should be read from HTTP context
/// </summary>
Expand All @@ -117,5 +127,10 @@ public class BodyLoggerOptions
/// Defines the text to append in case the body should be truncated <seealso cref="MaxBytes"/>
/// </summary>
public string Appendix { get; set; } = "\n---8<------------------------\nTRUNCATED DUE TO MAXBYTES LIMIT";

/// <summary>
/// Controls storage of client IP addresses https://learn.microsoft.com/en-us/azure/azure-monitor/app/ip-collection?tabs=net
/// </summary>
public bool DisableIpMasking { get; set; } = false;
}
```
20 changes: 10 additions & 10 deletions src/ApplicationInsightsRequestLogging/BodyLoggerMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,44 @@ namespace Azureblue.ApplicationInsights.RequestLogging
{
public class BodyLoggerMiddleware : IMiddleware
{
private readonly IOptions<BodyLoggerOptions> _options;
private readonly BodyLoggerOptions _options;
private readonly IBodyReader _bodyReader;
private readonly ITelemetryWriter _telemetryWriter;

public BodyLoggerMiddleware(IOptions<BodyLoggerOptions> options, IBodyReader bodyReader, ITelemetryWriter telemetryWriter)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_options = options.Value ?? throw new ArgumentNullException(nameof(options));
_bodyReader = bodyReader ?? throw new ArgumentNullException(nameof(bodyReader));
_telemetryWriter = telemetryWriter ?? throw new ArgumentNullException(nameof(telemetryWriter));
}

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var requestBody = string.Empty;
if (_options.Value.HttpVerbs.Contains(context.Request.Method))
if (_options.HttpVerbs.Contains(context.Request.Method))
{
// Temporarily store request body
requestBody = await _bodyReader.ReadRequestBodyAsync(context, _options.Value.MaxBytes, _options.Value.Appendix);
requestBody = await _bodyReader.ReadRequestBodyAsync(context, _options.MaxBytes, _options.Appendix);

_bodyReader.PrepareResponseBodyReading(context);
}

// hand over to the next middleware and wait for the call to return
await next(context);

if (_options.Value.HttpVerbs.Contains(context.Request.Method))
if (_options.HttpVerbs.Contains(context.Request.Method))
{
if (_options.Value.HttpCodes.Contains(context.Response.StatusCode))
if (_options.HttpCodes.Contains(context.Response.StatusCode))
{
var responseBody = await _bodyReader.ReadResponseBodyAsync(context, _options.Value.MaxBytes, _options.Value.Appendix);
var responseBody = await _bodyReader.ReadResponseBodyAsync(context, _options.MaxBytes, _options.Appendix);

_telemetryWriter.Write(context, _options.Value.RequestBodyPropertyKey, requestBody);
_telemetryWriter.Write(context, _options.Value.ResponseBodyPropertyKey, responseBody);
_telemetryWriter.Write(context, _options.RequestBodyPropertyKey, requestBody);
_telemetryWriter.Write(context, _options.ResponseBodyPropertyKey, responseBody);
}

// Copy back so response body is available for the user agent
// prevent 500 error when Not StatusCode of Interest
await this._bodyReader.RestoreOriginalResponseBodyStreamAsync(context);
await _bodyReader.RestoreOriginalResponseBodyStreamAsync(context);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using Microsoft.ApplicationInsights.Extensibility;

namespace Azureblue.ApplicationInsights.RequestLogging
{
Expand All @@ -8,9 +9,7 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddAppInsightsHttpBodyLogging(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}

services.AddOptions();
AddBodyLogger(services);
Expand All @@ -21,31 +20,28 @@ public static IServiceCollection AddAppInsightsHttpBodyLogging(this IServiceColl
public static IServiceCollection AddAppInsightsHttpBodyLogging(this IServiceCollection services, Action<BodyLoggerOptions> setupAction)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}

if (setupAction == null)
{
throw new ArgumentNullException(nameof(setupAction));
}

AddBodyLogger(services, setupAction);

return services;
}

internal static void AddBodyLogger(IServiceCollection services)
{
services.AddScoped<BodyLoggerMiddleware>();
services.AddScoped<IBodyReader, BodyReader>();
services.AddScoped<ITelemetryWriter, TelemetryWriter>();
}

internal static void AddBodyLogger(IServiceCollection services, Action<BodyLoggerOptions> setupAction)
private static void AddBodyLogger(IServiceCollection services, Action<BodyLoggerOptions> setupAction)
{
AddBodyLogger(services);
services.Configure(setupAction);
}

private static void AddBodyLogger(IServiceCollection services)
{
services.AddSingleton<BodyLoggerMiddleware>();
services.AddSingleton<IBodyReader, BodyReader>();
services.AddSingleton<ITelemetryWriter, TelemetryWriter>();
services.AddSingleton<ITelemetryInitializer, ClientIpInitializer>();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Extensions.Options;

namespace Azureblue.ApplicationInsights.RequestLogging
{
public class ClientIpInitializer : ITelemetryInitializer
{
private readonly BodyLoggerOptions _options;

public ClientIpInitializer(IOptions<BodyLoggerOptions> options) => _options = options.Value;

public void Initialize(ITelemetry telemetry)
{
if (!_options.DisableIpMasking) return;

var clientIpKey = _options.ClientIpPropertyKey;
if (telemetry is ISupportProperties propTelemetry && !propTelemetry.Properties.ContainsKey(clientIpKey))
{
var clientIpValue = telemetry.Context.Location.Ip;
propTelemetry.Properties.Add(clientIpKey, clientIpValue);
}
}
}
}
10 changes: 10 additions & 0 deletions src/ApplicationInsightsRequestLogging/Options/BodyLoggerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ public BodyLoggerOptions()
/// </summary>
public string ResponseBodyPropertyKey { get; set; } = "ResponseBody";

/// <summary>
/// Which property key should be used
/// </summary>
public string ClientIpPropertyKey { get; set; } = "ClientIp";

/// <summary>
/// Defines the amount of bytes that should be read from HTTP context
/// </summary>
Expand All @@ -45,5 +50,10 @@ public BodyLoggerOptions()
/// Defines the text to append in case the body should be truncated <seealso cref="MaxBytes"/>
/// </summary>
public string Appendix { get; set; } = "\n---8<------------------------\nTRUNCATED DUE TO MAXBYTES LIMIT";

/// <summary>
/// Controls storage of client IP addresses https://learn.microsoft.com/en-us/azure/azure-monitor/app/ip-collection?tabs=net
/// </summary>
public bool DisableIpMasking { get; set; } = false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.2.0" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.21.0" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="3.1.21" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Azureblue.ApplicationInsights.RequestLogging;
using FluentAssertions;
using System;
using System.Linq;
using Xunit;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Hosting;
Expand All @@ -19,10 +20,10 @@ public class BodyLoggerMiddlewareTests
public void BodyLoggerMiddleware_Should_Throw_If_Ctor_Params_Null()
{
// Arrange & Act
Action action = () => { var middleware = new BodyLoggerMiddleware(null, null, null); };
var action = () => { var middleware = new BodyLoggerMiddleware(null, null, null); };

// Assert
action.Should().Throw<ArgumentNullException>();
action.Should().Throw<NullReferenceException>();
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace ApplicationInsightsRequestLoggingTests;

public class FakeRemoteIpAddressMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
context.Connection.RemoteIpAddress = IPAddress.Parse("127.168.1.32");
await next(context);
}
}
22 changes: 22 additions & 0 deletions test/ManualTests/Controllers/TestController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Mvc;

namespace ManualTests.Controllers
{
[Route("api/")]
[ApiController]
public class TestController : ControllerBase
{
[HttpPost("test")]
public ActionResult<TestData> Test([FromBody] TestData data)
{
return Ok(data);
}
}
}

public class TestData
{
public string Name { get; set; }
public string Blog { get; set; }
public string Topics { get; set; }
}
18 changes: 18 additions & 0 deletions test/ManualTests/ManualTests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.21.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\ApplicationInsightsRequestLogging\ApplicationInsightsRequestLogging.csproj" />
</ItemGroup>

</Project>
Loading

0 comments on commit 96993b1

Please sign in to comment.