Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/Elastic.Apm/Extensions/TransactionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ internal static class TransactionExtensions
internal static void CollectRequestBody(this ITransaction transaction, bool isForError, IHttpRequestAdapter httpRequest, IApmLogger logger)
{
if (!transaction.IsSampled)
{
return;
}

if (httpRequest == null || !httpRequest.HasValue)
return;
Expand All @@ -34,9 +36,19 @@ internal static void CollectRequestBody(this ITransaction transaction, bool isFo
// Is request body already captured?
// We check transaction.IsContextCreated to avoid creating empty Context (that accessing transaction.Context directly would have done).
var hasContext = transaction is Transaction { IsContextCreated: true } || transaction.Context != null;
if (hasContext
&& transaction.Context.Request.Body != null
&& !ReferenceEquals(transaction.Context.Request.Body, Consts.Redacted))
var hasCapturedBody = hasContext && transaction.Context.Request?.Body != null;

// If CaptureBody is set to "transactions" and it is an error then we shouldn't capture it.
// If the body has already been captured then it has to be redacted.
if (isForError && transaction.Configuration.CaptureBody.Equals(ConfigConsts.SupportedValues.CaptureBodyTransactions))
{
if (hasCapturedBody)
transaction.Context.Request.Body = Consts.Redacted;

return;
}

if (hasCapturedBody && !ReferenceEquals(transaction.Context.Request.Body, Consts.Redacted))
return;

if (transaction.Configuration.CaptureBody.Equals(ConfigConsts.SupportedValues.CaptureBodyOff))
Expand Down
30 changes: 28 additions & 2 deletions src/integrations/Elastic.Apm.AspNetCore/AspNetCoreHttpRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,38 @@ internal class AspNetCoreHttpRequest : IHttpRequestAdapter
{
private readonly HttpRequest _request;

internal AspNetCoreHttpRequest(HttpRequest request) => _request = request;
internal AspNetCoreHttpRequest(HttpRequest request, IConfiguration configuration)
Comment thread
JeremyBessonElastic marked this conversation as resolved.
{
_request = request;

if (configuration?.CaptureBody == ConfigConsts.SupportedValues.CaptureBodyErrors)
{
_request?.EnableBuffering();
}
}

public string ExtractBody(IConfiguration configuration, IApmLogger logger, out bool longerThanMaxLength)
{
longerThanMaxLength = false;
return _request?.ExtractRequestBody(configuration, out longerThanMaxLength);

var shouldBeBuffered = configuration?.CaptureBody == ConfigConsts.SupportedValues.CaptureBodyErrors;

// Enable buffering if CaptureBody is set to "errors"
if (shouldBeBuffered)
{
// Reset stream position to the beginning in case the body was already read
_request.Body.Position = 0;
}

var bodyContent = _request?.ExtractRequestBody(configuration, out longerThanMaxLength);

// Reset stream position if buffering was enabled
if (shouldBeBuffered)
{
_request.Body.Position = 0;
}

return bodyContent;
}

public bool HasValue => _request != null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ private bool HandleException(PropertyFetcher propertyFetcher, PropertyFetcher ex
return false;
if (iTransaction is Transaction transaction)
{
transaction.CollectRequestBody(true, new AspNetCoreHttpRequest(exception.Request), Logger);
transaction.CollectRequestBody(true, new AspNetCoreHttpRequest(exception.Request, transaction.Configuration), Logger);
transaction.CaptureException(httpContextException, isHandled: isHandled);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ private static void FillSampledTransactionContextRequest(HttpContext context, Tr
Headers = GetHeaders(context.Request.Headers, transaction.Configuration)
};

transaction.CollectRequestBody(false, new AspNetCoreHttpRequest(context.Request), logger);
transaction.CollectRequestBody(false, new AspNetCoreHttpRequest(context.Request, transaction.Configuration), logger);
}
catch (Exception ex)
{
Expand Down
201 changes: 200 additions & 1 deletion test/integrations/Elastic.Apm.AspNetCore.Tests/BodyCapturingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
Expand Down Expand Up @@ -285,6 +284,206 @@ public async Task ApmMiddleware_ShouldSkipCapturing_WhenInvalidContentType()
result.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType);
}

[Fact]
public async Task When_CaptureBodyConfigurationToAllAndSuccessfulCall_Should_CaptureBody()
{
var sutEnv = StartSutEnv(CreateConfiguration());

// build test data, which we send to the sample app
var data = new { Name = "John" };

var body = JsonConvert.SerializeObject(data,
new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });

// send data to the sample app
var result = await sutEnv.HttpClient.PostAsync("api/Home/Send", new StringContent(body, Encoding.UTF8, "application/json"));

// wait for the payload sender to receive the transaction
sutEnv.MockPayloadSender.WaitForTransactions(TimeSpan.FromSeconds(10));

// make sure the sample app received the data
result.StatusCode.Should().Be((HttpStatusCode)200);

// and make sure the data is captured by the agent
sutEnv.MockPayloadSender.FirstTransaction.Should().NotBeNull();
sutEnv.MockPayloadSender.FirstTransaction.Context.Request.Body.Should().Be(body);
}

[Fact]
public async Task When_CaptureBodyConfigurationToTransactionsAndSuccessfulCall_Should_CaptureBody()
{
var sutEnv = StartSutEnv(CreateConfiguration(ConfigConsts.SupportedValues.CaptureBodyTransactions));

// build test data, which we send to the sample app
var data = new { Name = "John" };

var body = JsonConvert.SerializeObject(data,
new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });

// send data to the sample app
var result = await sutEnv.HttpClient.PostAsync("api/Home/Send", new StringContent(body, Encoding.UTF8, "application/json"));

// wait for the payload sender to receive the transaction
sutEnv.MockPayloadSender.WaitForTransactions(TimeSpan.FromSeconds(3));

// make sure the sample app received the data
result.StatusCode.Should().Be((HttpStatusCode)200);

// and make sure the data is captured by the agent
sutEnv.MockPayloadSender.FirstTransaction.Should().NotBeNull();
sutEnv.MockPayloadSender.FirstTransaction.Context.Request.Body.Should().Be(body);
}

[Fact]
public async Task When_CaptureBodyConfigurationToOffAndSuccessfulCall_ShouldNot_CaptureBody()
{
var sutEnv = StartSutEnv(CreateConfiguration(ConfigConsts.SupportedValues.CaptureBodyOff));

// build test data, which we send to the sample app
var data = new { Name = "John" };

var body = JsonConvert.SerializeObject(data,
new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });

// send data to the sample app
var result = await sutEnv.HttpClient.PostAsync("api/Home/Send", new StringContent(body, Encoding.UTF8, "application/json"));

// wait for the payload sender to receive the transaction
sutEnv.MockPayloadSender.WaitForTransactions(TimeSpan.FromSeconds(3));

// make sure the sample app received the data
result.StatusCode.Should().Be((HttpStatusCode)200);

// and make sure the data is captured by the agent
sutEnv.MockPayloadSender.FirstTransaction.Should().NotBeNull();
sutEnv.MockPayloadSender.FirstTransaction.Context.Request.Body.Should().Be("[REDACTED]");
}

[Fact]
public async Task When_CaptureBodyConfigurationToErrorsAndSuccessfulCall_ShouldNot_CaptureBody()
{
var sutEnv = StartSutEnv(CreateConfiguration(ConfigConsts.SupportedValues.CaptureBodyErrors));

// build test data, which we send to the sample app
var data = new { Name = "John" };

var body = JsonConvert.SerializeObject(data,
new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });

// send data to the sample app
var result = await sutEnv.HttpClient.PostAsync("api/Home/Send", new StringContent(body, Encoding.UTF8, "application/json"));

// wait for the payload sender to receive the transaction
sutEnv.MockPayloadSender.WaitForTransactions(TimeSpan.FromSeconds(3));

// make sure the sample app received the data
result.StatusCode.Should().Be((HttpStatusCode)200);

// and make sure the data is captured by the agent
sutEnv.MockPayloadSender.FirstTransaction.Should().NotBeNull();
sutEnv.MockPayloadSender.FirstTransaction.Context.Request.Body.Should().BeNull();
}

[Fact]
public async Task When_CaptureBodyConfigurationToAllAndFailingCall_Should_CaptureBody()
{
var sutEnv = StartSutEnv(CreateConfiguration());

// build test data, which we send to the sample app
var data = new { Name = "John" };

var body = JsonConvert.SerializeObject(data,
new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });

// send data to the sample app
var result = await sutEnv.HttpClient.PostAsync("api/Home/SendError", new StringContent(body, Encoding.UTF8, "application/json"));

// wait for the payload sender to receive the transaction
sutEnv.MockPayloadSender.WaitForTransactions(TimeSpan.FromSeconds(3));

// make sure the sample app received the data
result.StatusCode.Should().Be((HttpStatusCode)500);

// and make sure the data is captured by the agent
sutEnv.MockPayloadSender.FirstTransaction.Should().NotBeNull();
sutEnv.MockPayloadSender.FirstTransaction.Context.Request.Body.Should().Be(body);
}

[Fact]
public async Task When_CaptureBodyConfigurationToErrorsAndFailingCall_Should_CaptureBody()
{
var sutEnv = StartSutEnv(CreateConfiguration(ConfigConsts.SupportedValues.CaptureBodyErrors));

// build test data, which we send to the sample app
var data = new { Name = "John" };

var body = JsonConvert.SerializeObject(data,
new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });

// send data to the sample app
var result = await sutEnv.HttpClient.PostAsync("api/Home/SendError", new StringContent(body, Encoding.UTF8, "application/json"));

// wait for the payload sender to receive the transaction
sutEnv.MockPayloadSender.WaitForTransactions(TimeSpan.FromSeconds(3));

// make sure the sample app received the data
result.StatusCode.Should().Be((HttpStatusCode)500);

// and make sure the data is captured by the agent
sutEnv.MockPayloadSender.FirstTransaction.Should().NotBeNull();
sutEnv.MockPayloadSender.FirstTransaction.Context.Request.Body.Should().Be(body);
}

[Fact]
public async Task When_CaptureBodyConfigurationToTransactionsAndFailingCall_ShouldNot_CaptureBody()
{
var sutEnv = StartSutEnv(CreateConfiguration(ConfigConsts.SupportedValues.CaptureBodyTransactions));

// build test data, which we send to the sample app
var data = new { Name = "John" };

var body = JsonConvert.SerializeObject(data,
new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });

// send data to the sample app
var result = await sutEnv.HttpClient.PostAsync("api/Home/SendError", new StringContent(body, Encoding.UTF8, "application/json"));

// wait for the payload sender to receive the transaction
sutEnv.MockPayloadSender.WaitForTransactions(TimeSpan.FromSeconds(3));

// make sure the sample app received the data
result.StatusCode.Should().Be((HttpStatusCode)500);

// and make sure the data is captured by the agent
sutEnv.MockPayloadSender.FirstTransaction.Should().NotBeNull();
sutEnv.MockPayloadSender.FirstTransaction.Context.Request.Body.Should().Be("[REDACTED]");
}

[Fact]
public async Task When_CaptureBodyConfigurationToOffAndFailingCall_ShouldNot_CaptureBody()
{
var sutEnv = StartSutEnv(CreateConfiguration(ConfigConsts.SupportedValues.CaptureBodyOff));

// build test data, which we send to the sample app
var data = new { Name = "John" };

var body = JsonConvert.SerializeObject(data,
new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });

// send data to the sample app
var result = await sutEnv.HttpClient.PostAsync("api/Home/SendError", new StringContent(body, Encoding.UTF8, "application/json"));

// wait for the payload sender to receive the transaction
sutEnv.MockPayloadSender.WaitForTransactions(TimeSpan.FromSeconds(3));

// make sure the sample app received the data
result.StatusCode.Should().Be((HttpStatusCode)500);

// and make sure the data is captured by the agent
sutEnv.MockPayloadSender.FirstTransaction.Should().NotBeNull();
sutEnv.MockPayloadSender.FirstTransaction.Context.Request.Body.Should().Be("[REDACTED]");
}

private static IEnumerable<OptionsTestVariant> BuildOptionsTestVariants()
{
var captureBodyContentTypesVariants = new[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,13 @@ private async Task T3()
[HttpPost("api/Home/Send")]
public IActionResult Send([FromBody] BaseReportFilter<SendMessageFilter> filter) => filter == null ? StatusCode(500) : Ok();

/// <summary>
/// A test case to make sure that CaptureBoby is working as expected in case of failure.
/// </summary>
/// <returns>HTTP500</returns>
[HttpPost("api/Home/SendError")]
public IActionResult SendError([FromBody] BaseReportFilter<SendMessageFilter> filter) => throw new Exception("This is a post method test exception!");

/// <summary>
/// A test case to make sure that setting <see cref="IExecutionSegment.Outcome"/> manually is not overwritten by auto instrumentation.
/// </summary>
Expand Down
Loading