Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose Transaction.Context.Request and Transaction.Context.Response #134

Merged
merged 16 commits into from
Mar 4, 2019
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
2 changes: 2 additions & 0 deletions sample/ApiSamples/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ public static void SampleError()
public static void SampleCustomTransactionWithConvenientApi() => Agent.Tracer.CaptureTransaction("TestTransaction", "TestType",
t =>
{
t.Context.Response = new Response() { Finished = true, StatusCode = 200 };
t.Context.Request = new Request("GET", new Url{Protocol = "HTTP"});
t.Tags["fooTransaction"] = "barTransaction";
Thread.Sleep(10);
t.CaptureSpan("TestSpan", "TestSpanName", s =>
Expand Down
19 changes: 9 additions & 10 deletions src/Elastic.Apm.AspNetCore/ApmMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.Threading.Tasks;
using Elastic.Apm.Api;
using Elastic.Apm.Helpers;
using Elastic.Apm.Model.Payload;
using Microsoft.AspNetCore.Http;

[assembly:
Expand Down Expand Up @@ -32,21 +31,21 @@ public async Task InvokeAsync(HttpContext context)
var transaction = _tracer.StartTransactionInternal($"{context.Request.Method} {context.Request.Path}",
ApiConstants.TypeRequest);

transaction.Context.Request = new Request
var url = new Url
{
Full = context.Request?.Path.Value,
HostName = context.Request.Host.Host,
Protocol = GetProtocolName(context.Request.Protocol),
Raw = context.Request?.Path.Value //TODO
};

transaction.Context.Request = new Request( context.Request.Method, url)
{
Method = context.Request.Method,
Socket = new Socket
{
Encrypted = context.Request.IsHttps,
RemoteAddress = context.Connection?.RemoteIpAddress?.ToString()
},
Url = new Url
{
Full = context.Request?.Path.Value,
HostName = context.Request.Host.Host,
Protocol = GetProtocolName(context.Request.Protocol),
Raw = context.Request?.Path.Value //TODO
},
HttpVersion = GetHttpVersion(context.Request.Protocol)
};

Expand Down
28 changes: 28 additions & 0 deletions src/Elastic.Apm/Api/Context.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;

namespace Elastic.Apm.Api
{
public class Context
{
private readonly Lazy<Dictionary<string, string>> tags = new Lazy<Dictionary<string, string>>();

/// <summary>
/// If a log record was generated as a result of a http request, the http interface can be used to collect this
/// information.
/// This property is by default null! You have to assign a <see cref="Request" /> instance to this property in order to use
/// it.
/// </summary>
public Request Request { get; set; }

/// <summary>
/// If a log record was generated as a result of a http request, the http interface can be used to collect this
/// information.
/// This property is by default null! You have to assign a <see cref="Response" /> instance to this property in order to use
/// it.
/// </summary>
public Response Response { get; set; }

public Dictionary<string, string> Tags => tags.Value;
}
}
6 changes: 6 additions & 0 deletions src/Elastic.Apm/Api/ITransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ namespace Elastic.Apm.Api
{
public interface ITransaction
{
/// <summary>
/// Any arbitrary contextual information regarding the event, captured by the agent, optionally provided by the user.
/// This field is lazily initialized, you don't have to assign a value to it and you don't have to null check it either.
/// </summary>
Context Context { get; }

/// <summary>
/// The duration of the transaction.
/// If it's not set (its HasValue property is false) then the value
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
using Newtonsoft.Json;

namespace Elastic.Apm.Model.Payload
namespace Elastic.Apm.Api
{
internal class Request
/// <summary>
/// Encapsulates Request related information that can be attached to an <see cref="ITransaction" /> through <see cref="ITransaction.Context" />
/// See <see cref="Context.Request" />
/// </summary>
public class Request
{
public Request(string method, Url url) => (Method, Url) = (method, url);

public string HttpVersion { get; set; }

public string Method { get; set; }
public Socket Socket { get; set; }
public Url Url { get; set; }

public object Body { get; set; }
}

internal class Socket
public class Socket
{
public bool Encrypted { get; set; }

[JsonProperty("Remote_address")]
public string RemoteAddress { get; set; }
}

internal class Url
public class Url
{
public string Full { get; set; }
public string HostName { get; set; }
Expand Down
19 changes: 19 additions & 0 deletions src/Elastic.Apm/Api/Response.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Newtonsoft.Json;

namespace Elastic.Apm.Api
{
/// <summary>
/// Encapsulates Response related information that can be attached to an <see cref="ITransaction" /> through <see cref="ITransaction.Context" />
/// See <see cref="Context.Response" />
/// </summary>
public class Response
{
public bool Finished { get; set; }

/// <summary>
/// The HTTP status code of the response.
/// </summary>
[JsonProperty("Status_code")]
public int StatusCode { get; set; }
}
}
15 changes: 0 additions & 15 deletions src/Elastic.Apm/Model/Payload/Context.cs

This file was deleted.

12 changes: 0 additions & 12 deletions src/Elastic.Apm/Model/Payload/Response.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Elastic.Apm.Model.Payload;
using Elastic.Apm.Tests.Mocks;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using SampleAspNetCoreApp;
using Xunit;
Expand Down Expand Up @@ -34,18 +36,24 @@ public async Task TestErrorInAspNetCore()

var response = await client.GetAsync("/Home/TriggerError");

Assert.Single(capturedPayload.Payloads);
Assert.Single(capturedPayload.Payloads[0].Transactions);
capturedPayload.Should().NotBeNull();
capturedPayload.Payloads.Should().ContainSingle();
capturedPayload.Payloads[0].Transactions.Should().ContainSingle();

Assert.Single(capturedPayload.Errors);
Assert.Single(capturedPayload.Errors[0].Errors);
capturedPayload.Errors.Should().ContainSingle();
capturedPayload.Errors[0].Errors.Should().ContainSingle();

Assert.Equal("This is a test exception!", capturedPayload.Errors[0].Errors[0].Exception.Message);
Assert.Equal(typeof(Exception).FullName, capturedPayload.Errors[0].Errors[0].Exception.Type);
var errorException = capturedPayload.Errors[0].Errors[0].Exception;
errorException.Message.Should().Be("This is a test exception!");
errorException.Type.Should().Be(typeof(Exception).FullName);

Assert.Equal("/Home/TriggerError", capturedPayload.FirstErrorDetail.Context.Request.Url.Full);
Assert.Equal(HttpMethod.Get.Method, capturedPayload.FirstErrorDetail.Context.Request.Method);
Assert.False((capturedPayload.FirstErrorDetail.Exception as CapturedException)?.Handled);
var context = capturedPayload.FirstErrorDetail.Context;
context.Request.Url.Full.Should().Be("/Home/TriggerError");
context.Request.Method.Should().Be(HttpMethod.Get.Method);

var capturedException = errorException as CapturedException;
capturedException.Should().NotBeNull();
capturedException.Handled.Should().BeFalse();
}
}
}
Expand Down
76 changes: 44 additions & 32 deletions test/Elastic.Apm.AspNetCore.Tests/AspNetCoreMiddlewareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
using System.Threading.Tasks;
using Elastic.Apm.Model.Payload;
using Elastic.Apm.Tests.Mocks;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using SampleAspNetCoreApp;
using Xunit;
using Xunit.Sdk;

namespace Elastic.Apm.AspNetCore.Tests
{
Expand Down Expand Up @@ -43,41 +45,48 @@ public async Task HomeSimplePageTransactionTest()
{
var response = await _client.GetAsync("/Home/SimplePage");

Assert.Single(_capturedPayload.Payloads);
Assert.Single(_capturedPayload.Payloads[0].Transactions);
_capturedPayload.Payloads.Should().ContainSingle();
_capturedPayload.Payloads[0].Transactions.Should().ContainSingle();

var payload = _capturedPayload.Payloads[0];

//test payload
Assert.Equal(Assembly.GetEntryAssembly()?.GetName()?.Name, payload.Service.Name);
Assert.Equal(Consts.AgentName, payload.Service.Agent.Name);
Assert.Equal(Assembly.Load("Elastic.Apm").GetName().Version.ToString(), payload.Service.Agent.Version);
Assembly.CreateQualifiedName("ASP.NET Core", payload.Service.Framework.Name);
Assembly.CreateQualifiedName(Assembly.Load("Microsoft.AspNetCore").GetName().Version.ToString(), payload.Service.Framework.Version);
payload.Service.Name.Should().NotBeNullOrWhiteSpace()
.And.Be(Assembly.GetEntryAssembly()?.GetName()?.Name);

payload.Service.Agent.Name.Should().Be(Consts.AgentName);
var apmVersion = Assembly.Load("Elastic.Apm").GetName().Version.ToString();
payload.Service.Agent.Version.Should().Be(apmVersion);

payload.Service.Framework.Name.Should().Be("ASP.NET Core");

var aspNetCoreVersion = Assembly.Load("Microsoft.AspNetCore").GetName().Version.ToString();
payload.Service.Framework.Version.Should().Be(aspNetCoreVersion);

var transaction = _capturedPayload.FirstTransaction;

//test transaction
Assert.Equal($"{response.RequestMessage.Method} {response.RequestMessage.RequestUri.AbsolutePath}", transaction.Name);
Assert.Equal("HTTP 2xx", transaction.Result);
Assert.True(transaction.Duration > 0);
Assert.Equal("request", transaction.Type);
Assert.True(transaction.Id != Guid.Empty);
var transactionName = $"{response.RequestMessage.Method} {response.RequestMessage.RequestUri.AbsolutePath}";
transaction.Name.Should().Be(transactionName);
transaction.Result.Should().Be("HTTP 2xx");
transaction.Duration.Should().BeGreaterThan(0);

transaction.Type.Should().Be("request");
transaction.Id.Should().NotBeEmpty();

//test transaction.context.response
Assert.Equal(200, transaction.Context.Response.StatusCode);
transaction.Context.Response.StatusCode.Should().Be(200);

//test transaction.context.request
Assert.Equal("2.0", transaction.Context.Request.HttpVersion);
Assert.Equal("GET", transaction.Context.Request.Method);
transaction.Context.Request.HttpVersion.Should().Be("2.0");
transaction.Context.Request.Method.Should().Be("GET");

//test transaction.context.request.url
Assert.Equal(response.RequestMessage.RequestUri.AbsolutePath, transaction.Context.Request.Url.Full);
Assert.Equal("localhost", transaction.Context.Request.Url.HostName);
Assert.Equal("HTTP", transaction.Context.Request.Url.Protocol);
transaction.Context.Request.Url.Full.Should().Be(response.RequestMessage.RequestUri.AbsolutePath);
transaction.Context.Request.Url.HostName.Should().Be("localhost");
transaction.Context.Request.Url.Protocol.Should().Be("HTTP");

//test transaction.context.request.encrypted
Assert.False(transaction.Context.Request.Socket.Encrypted);
transaction.Context.Request.Socket.Encrypted.Should().BeFalse();
}

/// <summary>
Expand All @@ -88,13 +97,9 @@ public async Task HomeSimplePageTransactionTest()
public async Task HomeIndexSpanTest()
{
var response = await _client.GetAsync("/Home/Index");
Assert.True(response.IsSuccessStatusCode);

var transaction = _capturedPayload.Payloads[0].Transactions[0];
Assert.NotEmpty(_capturedPayload.SpansOnFirstTransaction);
response.IsSuccessStatusCode.Should().BeTrue();

//one of the spans is a DB call:
Assert.Contains(_capturedPayload.SpansOnFirstTransaction, n => n.Context.Db != null);
_capturedPayload.SpansOnFirstTransaction.Should().NotBeEmpty().And.Contain(n => n.Context.Db != null);
}

/// <summary>
Expand All @@ -106,16 +111,23 @@ public async Task HomeIndexSpanTest()
public async Task FailingRequestWithoutConfiguredExceptionPage()
{
_client = Helper.GetClientWithoutExceptionPage(_agent, _factory);
await Assert.ThrowsAsync<Exception>(async () => { await _client.GetAsync("Home/TriggerError"); });

Assert.Single(_capturedPayload.Payloads);
Assert.Single(_capturedPayload.Payloads[0].Transactions);
Func<Task> act = async () => await _client.GetAsync("Home/TriggerError");
await act.Should().ThrowAsync<Exception>();

_capturedPayload.Payloads.Should().ContainSingle();
_capturedPayload.Payloads[0].Transactions.Should().ContainSingle();

Assert.NotEmpty(_capturedPayload.Errors);
Assert.Single(_capturedPayload.Errors[0].Errors);
_capturedPayload.Errors.Should().NotBeEmpty();
_capturedPayload.Errors[0].Errors.Should().ContainSingle();

//also make sure the tag is captured
Assert.Equal(((_capturedPayload.Errors[0] as Error)?.Errors[0] as Error.ErrorDetail)?.Context.Tags["foo"], "bar");
var error = _capturedPayload.Errors[0] as Error;
error.Should().NotBeNull();
var errorDetail = error.Errors[0] as Error.ErrorDetail;
errorDetail.Should().NotBeNull();
var tags = errorDetail.Context.Tags;
tags.Should().NotBeEmpty().And.ContainKey("foo").And.Contain("foo", "bar");
}

public void Dispose()
Expand Down
Loading