Skip to content

Commit 13a82db

Browse files
Fix the CaptureBody in AspNetCore (#2623)
Fix the CaptureBody in AspNetCore. Related to the issue #2029
1 parent 0d74b57 commit 13a82db

6 files changed

Lines changed: 252 additions & 8 deletions

File tree

src/Elastic.Apm/Extensions/TransactionExtensions.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ internal static class TransactionExtensions
2424
internal static void CollectRequestBody(this ITransaction transaction, bool isForError, IHttpRequestAdapter httpRequest, IApmLogger logger)
2525
{
2626
if (!transaction.IsSampled)
27+
{
2728
return;
29+
}
2830

2931
if (httpRequest == null || !httpRequest.HasValue)
3032
return;
@@ -34,9 +36,19 @@ internal static void CollectRequestBody(this ITransaction transaction, bool isFo
3436
// Is request body already captured?
3537
// We check transaction.IsContextCreated to avoid creating empty Context (that accessing transaction.Context directly would have done).
3638
var hasContext = transaction is Transaction { IsContextCreated: true } || transaction.Context != null;
37-
if (hasContext
38-
&& transaction.Context.Request.Body != null
39-
&& !ReferenceEquals(transaction.Context.Request.Body, Consts.Redacted))
39+
var hasCapturedBody = hasContext && transaction.Context.Request?.Body != null;
40+
41+
// If CaptureBody is set to "transactions" and it is an error then we shouldn't capture it.
42+
// If the body has already been captured then it has to be redacted.
43+
if (isForError && transaction.Configuration.CaptureBody.Equals(ConfigConsts.SupportedValues.CaptureBodyTransactions))
44+
{
45+
if (hasCapturedBody)
46+
transaction.Context.Request.Body = Consts.Redacted;
47+
48+
return;
49+
}
50+
51+
if (hasCapturedBody && !ReferenceEquals(transaction.Context.Request.Body, Consts.Redacted))
4052
return;
4153

4254
if (transaction.Configuration.CaptureBody.Equals(ConfigConsts.SupportedValues.CaptureBodyOff))

src/integrations/Elastic.Apm.AspNetCore/AspNetCoreHttpRequest.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,38 @@ internal class AspNetCoreHttpRequest : IHttpRequestAdapter
1414
{
1515
private readonly HttpRequest _request;
1616

17-
internal AspNetCoreHttpRequest(HttpRequest request) => _request = request;
17+
internal AspNetCoreHttpRequest(HttpRequest request, IConfiguration configuration)
18+
{
19+
_request = request;
20+
21+
if (configuration?.CaptureBody == ConfigConsts.SupportedValues.CaptureBodyErrors)
22+
{
23+
_request?.EnableBuffering();
24+
}
25+
}
1826

1927
public string ExtractBody(IConfiguration configuration, IApmLogger logger, out bool longerThanMaxLength)
2028
{
2129
longerThanMaxLength = false;
22-
return _request?.ExtractRequestBody(configuration, out longerThanMaxLength);
30+
31+
var shouldBeBuffered = configuration?.CaptureBody == ConfigConsts.SupportedValues.CaptureBodyErrors;
32+
33+
// Enable buffering if CaptureBody is set to "errors"
34+
if (shouldBeBuffered)
35+
{
36+
// Reset stream position to the beginning in case the body was already read
37+
_request.Body.Position = 0;
38+
}
39+
40+
var bodyContent = _request?.ExtractRequestBody(configuration, out longerThanMaxLength);
41+
42+
// Reset stream position if buffering was enabled
43+
if (shouldBeBuffered)
44+
{
45+
_request.Body.Position = 0;
46+
}
47+
48+
return bodyContent;
2349
}
2450

2551
public bool HasValue => _request != null;

src/integrations/Elastic.Apm.AspNetCore/DiagnosticListener/AspNetCoreDiagnosticListener.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ private bool HandleException(PropertyFetcher propertyFetcher, PropertyFetcher ex
9696
return false;
9797
if (iTransaction is Transaction transaction)
9898
{
99-
transaction.CollectRequestBody(true, new AspNetCoreHttpRequest(exception.Request), Logger);
99+
transaction.CollectRequestBody(true, new AspNetCoreHttpRequest(exception.Request, transaction.Configuration), Logger);
100100
transaction.CaptureException(httpContextException, isHandled: isHandled);
101101
}
102102

src/integrations/Elastic.Apm.AspNetCore/WebRequestTransactionCreator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ private static void FillSampledTransactionContextRequest(HttpContext context, Tr
122122
Headers = GetHeaders(context.Request.Headers, transaction.Configuration)
123123
};
124124

125-
transaction.CollectRequestBody(false, new AspNetCoreHttpRequest(context.Request), logger);
125+
transaction.CollectRequestBody(false, new AspNetCoreHttpRequest(context.Request, transaction.Configuration), logger);
126126
}
127127
catch (Exception ex)
128128
{

test/integrations/Elastic.Apm.AspNetCore.Tests/BodyCapturingTests.cs

Lines changed: 200 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
using System;
66
using System.Collections.Generic;
7-
using System.Diagnostics;
87
using System.IO;
98
using System.Linq;
109
using System.Net;
@@ -285,6 +284,206 @@ public async Task ApmMiddleware_ShouldSkipCapturing_WhenInvalidContentType()
285284
result.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType);
286285
}
287286

287+
[Fact]
288+
public async Task When_CaptureBodyConfigurationToAllAndSuccessfulCall_Should_CaptureBody()
289+
{
290+
var sutEnv = StartSutEnv(CreateConfiguration());
291+
292+
// build test data, which we send to the sample app
293+
var data = new { Name = "John" };
294+
295+
var body = JsonConvert.SerializeObject(data,
296+
new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });
297+
298+
// send data to the sample app
299+
var result = await sutEnv.HttpClient.PostAsync("api/Home/Send", new StringContent(body, Encoding.UTF8, "application/json"));
300+
301+
// wait for the payload sender to receive the transaction
302+
sutEnv.MockPayloadSender.WaitForTransactions(TimeSpan.FromSeconds(10));
303+
304+
// make sure the sample app received the data
305+
result.StatusCode.Should().Be((HttpStatusCode)200);
306+
307+
// and make sure the data is captured by the agent
308+
sutEnv.MockPayloadSender.FirstTransaction.Should().NotBeNull();
309+
sutEnv.MockPayloadSender.FirstTransaction.Context.Request.Body.Should().Be(body);
310+
}
311+
312+
[Fact]
313+
public async Task When_CaptureBodyConfigurationToTransactionsAndSuccessfulCall_Should_CaptureBody()
314+
{
315+
var sutEnv = StartSutEnv(CreateConfiguration(ConfigConsts.SupportedValues.CaptureBodyTransactions));
316+
317+
// build test data, which we send to the sample app
318+
var data = new { Name = "John" };
319+
320+
var body = JsonConvert.SerializeObject(data,
321+
new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });
322+
323+
// send data to the sample app
324+
var result = await sutEnv.HttpClient.PostAsync("api/Home/Send", new StringContent(body, Encoding.UTF8, "application/json"));
325+
326+
// wait for the payload sender to receive the transaction
327+
sutEnv.MockPayloadSender.WaitForTransactions(TimeSpan.FromSeconds(3));
328+
329+
// make sure the sample app received the data
330+
result.StatusCode.Should().Be((HttpStatusCode)200);
331+
332+
// and make sure the data is captured by the agent
333+
sutEnv.MockPayloadSender.FirstTransaction.Should().NotBeNull();
334+
sutEnv.MockPayloadSender.FirstTransaction.Context.Request.Body.Should().Be(body);
335+
}
336+
337+
[Fact]
338+
public async Task When_CaptureBodyConfigurationToOffAndSuccessfulCall_ShouldNot_CaptureBody()
339+
{
340+
var sutEnv = StartSutEnv(CreateConfiguration(ConfigConsts.SupportedValues.CaptureBodyOff));
341+
342+
// build test data, which we send to the sample app
343+
var data = new { Name = "John" };
344+
345+
var body = JsonConvert.SerializeObject(data,
346+
new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });
347+
348+
// send data to the sample app
349+
var result = await sutEnv.HttpClient.PostAsync("api/Home/Send", new StringContent(body, Encoding.UTF8, "application/json"));
350+
351+
// wait for the payload sender to receive the transaction
352+
sutEnv.MockPayloadSender.WaitForTransactions(TimeSpan.FromSeconds(3));
353+
354+
// make sure the sample app received the data
355+
result.StatusCode.Should().Be((HttpStatusCode)200);
356+
357+
// and make sure the data is captured by the agent
358+
sutEnv.MockPayloadSender.FirstTransaction.Should().NotBeNull();
359+
sutEnv.MockPayloadSender.FirstTransaction.Context.Request.Body.Should().Be("[REDACTED]");
360+
}
361+
362+
[Fact]
363+
public async Task When_CaptureBodyConfigurationToErrorsAndSuccessfulCall_ShouldNot_CaptureBody()
364+
{
365+
var sutEnv = StartSutEnv(CreateConfiguration(ConfigConsts.SupportedValues.CaptureBodyErrors));
366+
367+
// build test data, which we send to the sample app
368+
var data = new { Name = "John" };
369+
370+
var body = JsonConvert.SerializeObject(data,
371+
new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });
372+
373+
// send data to the sample app
374+
var result = await sutEnv.HttpClient.PostAsync("api/Home/Send", new StringContent(body, Encoding.UTF8, "application/json"));
375+
376+
// wait for the payload sender to receive the transaction
377+
sutEnv.MockPayloadSender.WaitForTransactions(TimeSpan.FromSeconds(3));
378+
379+
// make sure the sample app received the data
380+
result.StatusCode.Should().Be((HttpStatusCode)200);
381+
382+
// and make sure the data is captured by the agent
383+
sutEnv.MockPayloadSender.FirstTransaction.Should().NotBeNull();
384+
sutEnv.MockPayloadSender.FirstTransaction.Context.Request.Body.Should().BeNull();
385+
}
386+
387+
[Fact]
388+
public async Task When_CaptureBodyConfigurationToAllAndFailingCall_Should_CaptureBody()
389+
{
390+
var sutEnv = StartSutEnv(CreateConfiguration());
391+
392+
// build test data, which we send to the sample app
393+
var data = new { Name = "John" };
394+
395+
var body = JsonConvert.SerializeObject(data,
396+
new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });
397+
398+
// send data to the sample app
399+
var result = await sutEnv.HttpClient.PostAsync("api/Home/SendError", new StringContent(body, Encoding.UTF8, "application/json"));
400+
401+
// wait for the payload sender to receive the transaction
402+
sutEnv.MockPayloadSender.WaitForTransactions(TimeSpan.FromSeconds(3));
403+
404+
// make sure the sample app received the data
405+
result.StatusCode.Should().Be((HttpStatusCode)500);
406+
407+
// and make sure the data is captured by the agent
408+
sutEnv.MockPayloadSender.FirstTransaction.Should().NotBeNull();
409+
sutEnv.MockPayloadSender.FirstTransaction.Context.Request.Body.Should().Be(body);
410+
}
411+
412+
[Fact]
413+
public async Task When_CaptureBodyConfigurationToErrorsAndFailingCall_Should_CaptureBody()
414+
{
415+
var sutEnv = StartSutEnv(CreateConfiguration(ConfigConsts.SupportedValues.CaptureBodyErrors));
416+
417+
// build test data, which we send to the sample app
418+
var data = new { Name = "John" };
419+
420+
var body = JsonConvert.SerializeObject(data,
421+
new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });
422+
423+
// send data to the sample app
424+
var result = await sutEnv.HttpClient.PostAsync("api/Home/SendError", new StringContent(body, Encoding.UTF8, "application/json"));
425+
426+
// wait for the payload sender to receive the transaction
427+
sutEnv.MockPayloadSender.WaitForTransactions(TimeSpan.FromSeconds(3));
428+
429+
// make sure the sample app received the data
430+
result.StatusCode.Should().Be((HttpStatusCode)500);
431+
432+
// and make sure the data is captured by the agent
433+
sutEnv.MockPayloadSender.FirstTransaction.Should().NotBeNull();
434+
sutEnv.MockPayloadSender.FirstTransaction.Context.Request.Body.Should().Be(body);
435+
}
436+
437+
[Fact]
438+
public async Task When_CaptureBodyConfigurationToTransactionsAndFailingCall_ShouldNot_CaptureBody()
439+
{
440+
var sutEnv = StartSutEnv(CreateConfiguration(ConfigConsts.SupportedValues.CaptureBodyTransactions));
441+
442+
// build test data, which we send to the sample app
443+
var data = new { Name = "John" };
444+
445+
var body = JsonConvert.SerializeObject(data,
446+
new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });
447+
448+
// send data to the sample app
449+
var result = await sutEnv.HttpClient.PostAsync("api/Home/SendError", new StringContent(body, Encoding.UTF8, "application/json"));
450+
451+
// wait for the payload sender to receive the transaction
452+
sutEnv.MockPayloadSender.WaitForTransactions(TimeSpan.FromSeconds(3));
453+
454+
// make sure the sample app received the data
455+
result.StatusCode.Should().Be((HttpStatusCode)500);
456+
457+
// and make sure the data is captured by the agent
458+
sutEnv.MockPayloadSender.FirstTransaction.Should().NotBeNull();
459+
sutEnv.MockPayloadSender.FirstTransaction.Context.Request.Body.Should().Be("[REDACTED]");
460+
}
461+
462+
[Fact]
463+
public async Task When_CaptureBodyConfigurationToOffAndFailingCall_ShouldNot_CaptureBody()
464+
{
465+
var sutEnv = StartSutEnv(CreateConfiguration(ConfigConsts.SupportedValues.CaptureBodyOff));
466+
467+
// build test data, which we send to the sample app
468+
var data = new { Name = "John" };
469+
470+
var body = JsonConvert.SerializeObject(data,
471+
new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });
472+
473+
// send data to the sample app
474+
var result = await sutEnv.HttpClient.PostAsync("api/Home/SendError", new StringContent(body, Encoding.UTF8, "application/json"));
475+
476+
// wait for the payload sender to receive the transaction
477+
sutEnv.MockPayloadSender.WaitForTransactions(TimeSpan.FromSeconds(3));
478+
479+
// make sure the sample app received the data
480+
result.StatusCode.Should().Be((HttpStatusCode)500);
481+
482+
// and make sure the data is captured by the agent
483+
sutEnv.MockPayloadSender.FirstTransaction.Should().NotBeNull();
484+
sutEnv.MockPayloadSender.FirstTransaction.Context.Request.Body.Should().Be("[REDACTED]");
485+
}
486+
288487
private static IEnumerable<OptionsTestVariant> BuildOptionsTestVariants()
289488
{
290489
var captureBodyContentTypesVariants = new[]

test/integrations/applications/SampleAspNetCoreApp/Controllers/HomeController.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,13 @@ private async Task T3()
284284
[HttpPost("api/Home/Send")]
285285
public IActionResult Send([FromBody] BaseReportFilter<SendMessageFilter> filter) => filter == null ? StatusCode(500) : Ok();
286286

287+
/// <summary>
288+
/// A test case to make sure that CaptureBoby is working as expected in case of failure.
289+
/// </summary>
290+
/// <returns>HTTP500</returns>
291+
[HttpPost("api/Home/SendError")]
292+
public IActionResult SendError([FromBody] BaseReportFilter<SendMessageFilter> filter) => throw new Exception("This is a post method test exception!");
293+
287294
/// <summary>
288295
/// A test case to make sure that setting <see cref="IExecutionSegment.Outcome"/> manually is not overwritten by auto instrumentation.
289296
/// </summary>

0 commit comments

Comments
 (0)