Skip to content

Commit

Permalink
Add Code Level Metrics for CodeStream integration (#1135)
Browse files Browse the repository at this point in the history
* Adds default capture of code level metric attributes (#1098)

* Adds namespace and function attributes to spans.

Defaults usage to instrumented method TypeName and MethodName.

* Adds code level metrics enabled configuration defaulting to false.

* Adds user/customer code level span attributes for ASP.NET Core MVC Controller actions. (#1099)

* Adds user/customer code level span attributes for Web API 2 (#1100)

* Adds user/customer code level span attributes for Web API 2.

* Adds Owin hosted Web API 2 tests for CLM.

* Adds NEW_RELIC_CODE_LEVEL_METRICS_ENABLED environment variable configuration. (#1103)

* CLM support for ASP.NET MVC Framework applications (#1119)

* Add CLM for .NET MVC (Framework). Minor refactoring for performance. Commentary on possible work

* Finalize impl

* Integration tests

* CR Feedback. Update integration test workflow to run Code Level Metrics tests

* Updates Code Level Metrics tests to be more resilient and easier to track down issues. (#1124)

Adds waiting for span event log line. Prevents inlining for custom methods in AgentApiExecutor. Asserts individual span events where found.

* Changelog

* PR feedback

* PR feedback

Co-authored-by: Michael Goin <[email protected]>
Co-authored-by: Jacob Affinito <[email protected]>
Co-authored-by: Michael Goin <[email protected]>
  • Loading branch information
4 people authored Jun 8, 2022
1 parent bd438f2 commit e925389
Show file tree
Hide file tree
Showing 28 changed files with 816 additions and 24 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/all_solutions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,8 @@ jobs:
Logging.MetricsAndForwarding.Serilog, Logging.MetricsAndForwarding.NLog,
ReJit.NetFramework, RemoteServiceFixtures, RestSharp, WCF.Client.IIS.ASPDisabled, WCF.Client.IIS.ASPEnabled,
WCF.Client.Self, WCF.Service.IIS.ASPDisabled, WCF.Service.IIS.ASPEnabled, WCF.Service.Self, RequestHeadersCapture.WCF,
RequestHeadersCapture.Owin, RequestHeadersCapture.AspNetCore, RequestHeadersCapture.Asp35, RequestHeadersCapture.EnvironmentVariables, RequestHandling ]
RequestHeadersCapture.Owin, RequestHeadersCapture.AspNetCore, RequestHeadersCapture.Asp35, RequestHeadersCapture.EnvironmentVariables,
RequestHandling, CodeLevelMetrics ]
fail-fast: false # we don't want one test failure in one namespace to kill the other runs

env:
Expand Down
1 change: 1 addition & 0 deletions src/Agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### New Features
* Adds support for logging metrics, forwarding application logs, and enriching application logs written to disk or standard out for NLog versions v5 and v4. [#1087](https://github.com/newrelic/newrelic-dotnet-agent/pull/1087)
* Adds integration with CodeStream, introducing Code-Level Metrics! Golden Signals visible in your IDE through New Relic CodeStream. [Learn more here](https://docs.newrelic.com/docs/apm/agents/net-agent/other-features/net-codestream-integration). For any issues or direct feedback, please reach out to [email protected]
* Updates the following installation methods to check for and remove deprecated files. ([#1104](https://github.com/newrelic/newrelic-dotnet-agent/pull/1104))
* MSI Installer
* Azure Site Extension
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public interface IAttributeDefinitions
AttributeDefinition<string, string> CatReferringTransactionGuidForEvents { get; }
AttributeDefinition<string, string> CatReferringTransactionGuidForTraces { get; }
AttributeDefinition<string, string> ClientCrossProcessId { get; }
AttributeDefinition<string, string> CodeFunction { get; }
AttributeDefinition<string, string> CodeNamespace { get; }
AttributeDefinition<string, string> Component { get; }
AttributeDefinition<TimeSpan, double> CpuTime { get; }
AttributeDefinition<string, string> CustomEventType { get; }
Expand Down Expand Up @@ -957,5 +959,25 @@ public AttributeDefinition<TypeAttributeValue, string> GetTypeAttribute(TypeAttr
AttributeDefinitionBuilder.CreateString("type", AttributeClassification.Intrinsics)
.AppliesTo(AttributeDestinations.CustomEvent)
.Build(_attribFilter));

private AttributeDefinition<string, string> _codeFunction;
public AttributeDefinition<string, string> CodeFunction => _codeFunction ?? (
_codeFunction = AttributeDefinitionBuilder.CreateString(
"code.function",
AttributeClassification.AgentAttributes
)
.AppliesTo(AttributeDestinations.SpanEvent)
.Build(_attribFilter)
);

private AttributeDefinition<string, string> _codeNamespace;
public AttributeDefinition<string, string> CodeNamespace => _codeNamespace ?? (
_codeNamespace = AttributeDefinitionBuilder.CreateString(
"code.namespace",
AttributeClassification.AgentAttributes
)
.AppliesTo(AttributeDestinations.SpanEvent)
.Build(_attribFilter)
);
}
}
57 changes: 57 additions & 0 deletions src/Agent/NewRelic/Agent/Core/Config/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ public partial class configuration

private configurationProcessHost processHostField;

private configurationCodeLevelMetrics codeLevelMetricsField;

private bool agentEnabledField;

private bool rootAgentEnabledField;
Expand All @@ -113,6 +115,7 @@ public partial class configuration
/// </summary>
public configuration()
{
this.codeLevelMetricsField = new configurationCodeLevelMetrics();
this.processHostField = new configurationProcessHost();
this.utilizationField = new configurationUtilization();
this.appSettingsField = new List<configurationAdd>();
Expand Down Expand Up @@ -564,6 +567,18 @@ public configurationProcessHost processHost
}
}

public configurationCodeLevelMetrics codeLevelMetrics
{
get
{
return this.codeLevelMetricsField;
}
set
{
this.codeLevelMetricsField = value;
}
}

/// <summary>
/// Set this to true to enable the agent.
/// </summary>
Expand Down Expand Up @@ -5447,6 +5462,48 @@ public virtual configurationProcessHost Clone()
#endregion
}

[System.CodeDom.Compiler.GeneratedCodeAttribute("Xsd2Code", "3.6.0.20097")]
[System.SerializableAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="urn:newrelic-config")]
public partial class configurationCodeLevelMetrics
{

private bool enabledField;

/// <summary>
/// configurationCodeLevelMetrics class constructor
/// </summary>
public configurationCodeLevelMetrics()
{
this.enabledField = false;
}

[System.Xml.Serialization.XmlAttributeAttribute()]
[System.ComponentModel.DefaultValueAttribute(false)]
public bool enabled
{
get
{
return this.enabledField;
}
set
{
this.enabledField = value;
}
}

#region Clone method
/// <summary>
/// Create a clone of this configurationCodeLevelMetrics object
/// </summary>
public virtual configurationCodeLevelMetrics Clone()
{
return ((configurationCodeLevelMetrics)(this.MemberwiseClone()));
}
#endregion
}

[System.CodeDom.Compiler.GeneratedCodeAttribute("Xsd2Code", "3.6.0.20097")]
[System.SerializableAttribute()]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="urn:newrelic-config")]
Expand Down
19 changes: 19 additions & 0 deletions src/Agent/NewRelic/Agent/Core/Config/Configuration.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -1788,6 +1788,25 @@
</xs:attribute>
</xs:complexType>
</xs:element>

<xs:element name="codeLevelMetrics" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
Turns on/off capture of code level metric attributes.
</xs:documentation>
</xs:annotation>

<xs:complexType>
<xs:attribute name="enabled" type="xs:boolean" default="false">
<xs:annotation>
<xs:documentation>
Set this to "true" to capture namespace and function information
on spans to enable code level metrics in CodeStream.
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:all>

<xs:attribute name="agentEnabled" type="xs:boolean" default="true">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2449,6 +2449,20 @@ private void LogDisabledPropertyUse(string disabledPropertyName, string newPrope
public int DatabaseStatementCacheCapcity => _databaseStatementCacheCapcity ?? (_databaseStatementCacheCapcity =
TryGetAppSettingAsIntWithDefault("SqlStatementCacheCapacity", DefaultSqlStatementCacheCapacity)).Value;

private bool? _codeLevelMetricsEnabled;
public bool CodeLevelMetricsEnabled
{
get
{
if (!_codeLevelMetricsEnabled.HasValue)
{
_codeLevelMetricsEnabled = EnvironmentOverrides(_localConfiguration.codeLevelMetrics.enabled, "NEW_RELIC_CODE_LEVEL_METRICS_ENABLED");
}

return _codeLevelMetricsEnabled.Value;
}
}

#endregion

private const bool CaptureTransactionTraceAttributesDefault = true;
Expand Down
3 changes: 3 additions & 0 deletions src/Agent/NewRelic/Agent/Core/Segments/NoOpSegment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public class NoOpSegment : ISegment, ISegmentExperimental, ISegmentDataState

public string TypeName => string.Empty;

public string UserCodeFunction { get => string.Empty; set { } }
public string UserCodeNamespace { get => string.Empty; set { } }

public void End() { }
public void End(Exception ex) { }
public void MakeCombinable() { }
Expand Down
21 changes: 21 additions & 0 deletions src/Agent/NewRelic/Agent/Core/Segments/Segment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,13 @@ public TimeSpan ExclusiveDurationOrZero
}
}

// For auto-instrumentation, we often instrument a function of the framework itself
// which represents and executes user code. So we need to keep track of the actual user
// code namespace (type) and function that the instrumentation represents for mapping to
// customer code.
public string UserCodeNamespace { get; set; } = null;
public string UserCodeFunction { get; set; } = null;

private void Finish()
{
var endTime = _transactionSegmentState.GetRelativeTime();
Expand Down Expand Up @@ -268,6 +275,20 @@ public SpanAttributeValueCollection GetAttributeValues()
}
}

if (_configurationSubscriber.Configuration.CodeLevelMetricsEnabled)
{
var codeNamespace = !string.IsNullOrEmpty(this.UserCodeNamespace)
? this.UserCodeNamespace
: this.MethodCallData.TypeName;

var codeFunction = !string.IsNullOrEmpty(this.UserCodeFunction)
? this.UserCodeFunction
: this.MethodCallData.MethodName;

AttribDefs.CodeNamespace.TrySetValue(attribValues, codeNamespace);
AttribDefs.CodeFunction.TrySetValue(attribValues, codeFunction);
}

Data.SetSpanTypeSpecificAttributes(attribValues);

return attribValues;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,19 @@ public interface ISegmentExperimental
/// <returns>The segment that the segmentData was added to.</returns>
ISegmentExperimental MakeLeaf();

/// <summary>
/// Get or set the function (method) name for the user/customer code represented
/// by the instrumentation. This only needs set when the instrumentation point and
/// the customer code represented differ. For example, controller actions.
/// </summary>
string UserCodeFunction { get; set; }

/// <summary>
/// Get or set the namespace (type) name for the user/customer code represented
/// by the instrumentation. This only needs set when the instrumentation point and
/// the customer code represented differ. For example, controller actions.
/// </summary>
string UserCodeNamespace { get; set; }

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,5 +190,6 @@ public interface IConfiguration
bool LogDecoratorEnabled { get; }
bool AppDomainCachingDisabled { get; }
bool ForceNewTransactionOnNewThread { get; }
bool CodeLevelMetricsEnabled { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using NewRelic.Agent.Api;
using NewRelic.Agent.Api.Experimental;
using NewRelic.Agent.Extensions.Providers.Wrapper;
using NewRelic.Reflection;
using System;
Expand Down Expand Up @@ -38,13 +39,18 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins

transaction.SetWebTransactionName(WebTransactionType.MVC, transactionName, TransactionNamePriority.FrameworkHigh);

var controllerTypeInfo = controllerContext.ActionDescriptor.ControllerTypeInfo;
//Framework uses ControllerType.Action for these metrics & transactions. WebApi is Controller.Action for both
//Taking opinionated stance to do ControllerType.MethodName for segments. Controller/Action for transactions
var controllerTypeName = controllerContext.ActionDescriptor.ControllerTypeInfo.Name;
var controllerTypeName = controllerTypeInfo.Name;
var methodName = controllerContext.ActionDescriptor.MethodInfo.Name;

var segment = transaction.StartMethodSegment(instrumentedMethodCall.MethodCall, controllerTypeName, methodName);

var segmentApi = segment.GetExperimentalApi();
segmentApi.UserCodeNamespace = controllerTypeInfo.FullName;
segmentApi.UserCodeFunction = methodName;

return Delegates.GetAsyncDelegateFor<Task>(agent, segment, TaskContinueWithOption.None);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using NewRelic.Agent.Api;
using NewRelic.Agent.Api.Experimental;
using NewRelic.Agent.Extensions.Providers.Wrapper;
using NewRelic.SystemExtensions;

Expand All @@ -29,17 +30,30 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins
var controllerContext = instrumentedMethodCall.MethodCall.MethodArguments.ExtractNotNullAs<dynamic>(0);
if (controllerContext != null)
{
var controllerName = MvcRouteNamingHelper.TryGetControllerNameFromObject(controllerContext);
var actionName = MvcRouteNamingHelper.TryGetActionNameFromRouteParameters(instrumentedMethodCall.MethodCall, controllerContext.RouteData);
// IMPORTANT: Resist the urge to blindly refactor all of this code to use `var`
// IMPORTANT: We are being intentional with types over using `var` here due to
// IMPORTANT: the effects of handling a `dynamic` object
string controllerName = MvcRouteNamingHelper.TryGetControllerNameFromObject(controllerContext);
string actionName = MvcRouteNamingHelper.TryGetActionNameFromRouteParameters(instrumentedMethodCall.MethodCall, controllerContext.RouteData);

var httpContext = controllerContext.HttpContext;
if (httpContext == null)
{
throw new NullReferenceException("httpContext");
}

var transactionName = string.Format("{0}/{1}", controllerName, actionName);
string transactionName = controllerName + "/" + actionName;
transaction.SetWebTransactionName(WebTransactionType.MVC, transactionName, TransactionNamePriority.FrameworkLow);

var segment = transaction.StartMethodSegment(instrumentedMethodCall.MethodCall, controllerName, actionName);
ISegment segment = transaction.StartMethodSegment(instrumentedMethodCall.MethodCall, controllerName, actionName);

string fullControllerName = MvcRouteNamingHelper.TryGetControllerFullNameFromObject(controllerContext);
if (fullControllerName != null)
{
ISegmentExperimental segmentApi = segment.GetExperimentalApi();
segmentApi.UserCodeNamespace = fullControllerName;
segmentApi.UserCodeFunction = actionName;
}

httpContext.Items[HttpContextSegmentKey] = segment;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins
{
var httpContext = HttpContext.Current;
if (httpContext == null)
{
throw new NullReferenceException("httpContext");
}

var segment = agent.CastAsSegment(httpContext.Items[AsyncBeginInvokeActionWrapper.HttpContextSegmentKey]);
httpContext.Items[AsyncBeginInvokeActionWrapper.HttpContextSegmentKey] = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins
{
// Handle a missing Controller after already being pushed through a valid Route
if (exception is HttpException he && he.GetHttpCode() == 404)
transaction.SetWebTransactionName(WebTransactionType.StatusCode, "404", TransactionNamePriority.FrameworkHigh);
{
transaction.SetWebTransactionName(WebTransactionType.StatusCode, "404",
TransactionNamePriority.FrameworkHigh);
}
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins
{
// Handle a missing Action after already being pushed through a valid Route and onto a Controller
if (exception is HttpException he && he.GetHttpCode() == 404)
transaction.SetWebTransactionName(WebTransactionType.StatusCode, "404", TransactionNamePriority.FrameworkHigh);
{
transaction.SetWebTransactionName(WebTransactionType.StatusCode, "404",
TransactionNamePriority.FrameworkHigh);
}
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo)
public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction)
{
var exception = instrumentedMethodCall.MethodCall.MethodArguments.ExtractAs<Exception>(2);
if (exception == null)
return Delegates.NoOp;

transaction.NoticeError(exception);

if (exception != null)
{
transaction.NoticeError(exception);
}
return Delegates.NoOp;
}
}
Expand Down
Loading

0 comments on commit e925389

Please sign in to comment.