diff --git a/.github/workflows/all_solutions.yml b/.github/workflows/all_solutions.yml index 655f28695e..9b33eda27f 100644 --- a/.github/workflows/all_solutions.yml +++ b/.github/workflows/all_solutions.yml @@ -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: diff --git a/src/Agent/CHANGELOG.md b/src/Agent/CHANGELOG.md index 5e012b8a67..526d96a7b4 100644 --- a/src/Agent/CHANGELOG.md +++ b/src/Agent/CHANGELOG.md @@ -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 support@codestream.com * 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 diff --git a/src/Agent/NewRelic/Agent/Core/Attributes/AttributeDefinitionService.cs b/src/Agent/NewRelic/Agent/Core/Attributes/AttributeDefinitionService.cs index 9da3538101..37e06c1688 100644 --- a/src/Agent/NewRelic/Agent/Core/Attributes/AttributeDefinitionService.cs +++ b/src/Agent/NewRelic/Agent/Core/Attributes/AttributeDefinitionService.cs @@ -34,6 +34,8 @@ public interface IAttributeDefinitions AttributeDefinition CatReferringTransactionGuidForEvents { get; } AttributeDefinition CatReferringTransactionGuidForTraces { get; } AttributeDefinition ClientCrossProcessId { get; } + AttributeDefinition CodeFunction { get; } + AttributeDefinition CodeNamespace { get; } AttributeDefinition Component { get; } AttributeDefinition CpuTime { get; } AttributeDefinition CustomEventType { get; } @@ -957,5 +959,25 @@ public AttributeDefinition GetTypeAttribute(TypeAttr AttributeDefinitionBuilder.CreateString("type", AttributeClassification.Intrinsics) .AppliesTo(AttributeDestinations.CustomEvent) .Build(_attribFilter)); + + private AttributeDefinition _codeFunction; + public AttributeDefinition CodeFunction => _codeFunction ?? ( + _codeFunction = AttributeDefinitionBuilder.CreateString( + "code.function", + AttributeClassification.AgentAttributes + ) + .AppliesTo(AttributeDestinations.SpanEvent) + .Build(_attribFilter) + ); + + private AttributeDefinition _codeNamespace; + public AttributeDefinition CodeNamespace => _codeNamespace ?? ( + _codeNamespace = AttributeDefinitionBuilder.CreateString( + "code.namespace", + AttributeClassification.AgentAttributes + ) + .AppliesTo(AttributeDestinations.SpanEvent) + .Build(_attribFilter) + ); } } diff --git a/src/Agent/NewRelic/Agent/Core/Config/Configuration.cs b/src/Agent/NewRelic/Agent/Core/Config/Configuration.cs index 6204f7fd4e..c5c3c2c767 100644 --- a/src/Agent/NewRelic/Agent/Core/Config/Configuration.cs +++ b/src/Agent/NewRelic/Agent/Core/Config/Configuration.cs @@ -94,6 +94,8 @@ public partial class configuration private configurationProcessHost processHostField; + private configurationCodeLevelMetrics codeLevelMetricsField; + private bool agentEnabledField; private bool rootAgentEnabledField; @@ -113,6 +115,7 @@ public partial class configuration /// public configuration() { + this.codeLevelMetricsField = new configurationCodeLevelMetrics(); this.processHostField = new configurationProcessHost(); this.utilizationField = new configurationUtilization(); this.appSettingsField = new List(); @@ -564,6 +567,18 @@ public configurationProcessHost processHost } } + public configurationCodeLevelMetrics codeLevelMetrics + { + get + { + return this.codeLevelMetricsField; + } + set + { + this.codeLevelMetricsField = value; + } + } + /// /// Set this to true to enable the agent. /// @@ -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; + + /// + /// configurationCodeLevelMetrics class constructor + /// + 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 + /// + /// Create a clone of this configurationCodeLevelMetrics object + /// + 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")] diff --git a/src/Agent/NewRelic/Agent/Core/Config/Configuration.xsd b/src/Agent/NewRelic/Agent/Core/Config/Configuration.xsd index 2c38247563..956426c35e 100644 --- a/src/Agent/NewRelic/Agent/Core/Config/Configuration.xsd +++ b/src/Agent/NewRelic/Agent/Core/Config/Configuration.xsd @@ -1788,6 +1788,25 @@ + + + + + Turns on/off capture of code level metric attributes. + + + + + + + + Set this to "true" to capture namespace and function information + on spans to enable code level metrics in CodeStream. + + + + + diff --git a/src/Agent/NewRelic/Agent/Core/Configuration/DefaultConfiguration.cs b/src/Agent/NewRelic/Agent/Core/Configuration/DefaultConfiguration.cs index 6c30f2ea62..44cd714860 100644 --- a/src/Agent/NewRelic/Agent/Core/Configuration/DefaultConfiguration.cs +++ b/src/Agent/NewRelic/Agent/Core/Configuration/DefaultConfiguration.cs @@ -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; diff --git a/src/Agent/NewRelic/Agent/Core/Segments/NoOpSegment.cs b/src/Agent/NewRelic/Agent/Core/Segments/NoOpSegment.cs index 74b461e429..d831a199b7 100644 --- a/src/Agent/NewRelic/Agent/Core/Segments/NoOpSegment.cs +++ b/src/Agent/NewRelic/Agent/Core/Segments/NoOpSegment.cs @@ -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() { } diff --git a/src/Agent/NewRelic/Agent/Core/Segments/Segment.cs b/src/Agent/NewRelic/Agent/Core/Segments/Segment.cs index b3ff4258a5..63f1bc6d2a 100644 --- a/src/Agent/NewRelic/Agent/Core/Segments/Segment.cs +++ b/src/Agent/NewRelic/Agent/Core/Segments/Segment.cs @@ -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(); @@ -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; diff --git a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/Experimental/ISegmentExperimental.cs b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/Experimental/ISegmentExperimental.cs index 047a889d20..2bf257e31e 100644 --- a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/Experimental/ISegmentExperimental.cs +++ b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/Experimental/ISegmentExperimental.cs @@ -30,5 +30,19 @@ public interface ISegmentExperimental /// The segment that the segmentData was added to. ISegmentExperimental MakeLeaf(); + /// + /// 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. + /// + string UserCodeFunction { get; set; } + + /// + /// 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. + /// + string UserCodeNamespace { get; set; } + } } diff --git a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Configuration/IConfiguration.cs b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Configuration/IConfiguration.cs index 98213fb387..861537753f 100644 --- a/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Configuration/IConfiguration.cs +++ b/src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Configuration/IConfiguration.cs @@ -190,5 +190,6 @@ public interface IConfiguration bool LogDecoratorEnabled { get; } bool AppDomainCachingDisabled { get; } bool ForceNewTransactionOnNewThread { get; } + bool CodeLevelMetricsEnabled { get; } } } diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore/InvokeActionMethodAsyncWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore/InvokeActionMethodAsyncWrapper.cs index 80f916581f..55330c5f30 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore/InvokeActionMethodAsyncWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/AspNetCore/InvokeActionMethodAsyncWrapper.cs @@ -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; @@ -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(agent, segment, TaskContinueWithOption.None); } diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/AsyncBeginInvokeActionWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/AsyncBeginInvokeActionWrapper.cs index 524a2a8182..ccaa997927 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/AsyncBeginInvokeActionWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/AsyncBeginInvokeActionWrapper.cs @@ -3,6 +3,7 @@ using System; using NewRelic.Agent.Api; +using NewRelic.Agent.Api.Experimental; using NewRelic.Agent.Extensions.Providers.Wrapper; using NewRelic.SystemExtensions; @@ -29,17 +30,30 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins var controllerContext = instrumentedMethodCall.MethodCall.MethodArguments.ExtractNotNullAs(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; } diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/AsyncEndInvokeActionWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/AsyncEndInvokeActionWrapper.cs index b63945cb63..d4e4fbdcf3 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/AsyncEndInvokeActionWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/AsyncEndInvokeActionWrapper.cs @@ -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; diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/GetControllerInstanceWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/GetControllerInstanceWrapper.cs index c2cf43fc5e..2c0293cb6c 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/GetControllerInstanceWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/GetControllerInstanceWrapper.cs @@ -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); + } }); } } diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/HandleUnknownActionWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/HandleUnknownActionWrapper.cs index 2e3dfa3aaf..8f15ff01aa 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/HandleUnknownActionWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/HandleUnknownActionWrapper.cs @@ -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); + } }); } } diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/InvokeExceptionFiltersWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/InvokeExceptionFiltersWrapper.cs index 2afa580a9e..58ec8dfdbd 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/InvokeExceptionFiltersWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/InvokeExceptionFiltersWrapper.cs @@ -22,11 +22,11 @@ public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo) public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction) { var exception = instrumentedMethodCall.MethodCall.MethodArguments.ExtractAs(2); - if (exception == null) - return Delegates.NoOp; - - transaction.NoticeError(exception); - + if (exception != null) + { + transaction.NoticeError(exception); + } + return Delegates.NoOp; } } diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/MvcRouteNamingHelper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/MvcRouteNamingHelper.cs index 78faa27a99..4395c44fd8 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/MvcRouteNamingHelper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/MvcRouteNamingHelper.cs @@ -15,17 +15,28 @@ public static string TryGetControllerNameFromObject(dynamic controllerContext) { var controller = controllerContext.Controller; if (controller == null) + { return "Unknown Controller"; + } var controllerType = controller.GetType(); return controllerType.Name; } + public static string TryGetControllerFullNameFromObject(dynamic controllerContext) + { + var controller = controllerContext.Controller; + var controllerType = controller?.GetType(); + return controllerType?.FullName; + } + public static string TryGetActionNameFromRouteParameters(MethodCall methodCall, dynamic routeData) { var actionName = methodCall.MethodArguments.ExtractAs(1); if (actionName != null) + { return actionName; + } var directRouteMatches = routeData?.Values?["MS_DirectRouteMatches"] as IEnumerable ?? Enumerable.Empty(); routeData = directRouteMatches?.FirstOrDefault() ?? routeData; diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/SyncInvokeActionWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/SyncInvokeActionWrapper.cs index a682db8e34..e85c613406 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/SyncInvokeActionWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Mvc3/SyncInvokeActionWrapper.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using NewRelic.Agent.Api; +using NewRelic.Agent.Api.Experimental; using NewRelic.Agent.Extensions.Providers.Wrapper; using NewRelic.SystemExtensions; @@ -25,16 +26,28 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins 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 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); + // segment should never be null.. it will either be a NoOp segment, or we would have thrown here... if (segment != null) { + string fullControllerName = MvcRouteNamingHelper.TryGetControllerFullNameFromObject(controllerContext); + if (fullControllerName != null) + { + ISegmentExperimental segmentApi = segment.GetExperimentalApi(); + segmentApi.UserCodeNamespace = fullControllerName; + segmentApi.UserCodeFunction = actionName; + } + return Delegates.GetDelegateFor(segment); } } diff --git a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/WebApi2/InvokeActionAsyncWrapper.cs b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/WebApi2/InvokeActionAsyncWrapper.cs index 22c158b219..c7ec1502e6 100644 --- a/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/WebApi2/InvokeActionAsyncWrapper.cs +++ b/src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/WebApi2/InvokeActionAsyncWrapper.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using System.Web.Http.Controllers; using NewRelic.Agent.Api; +using NewRelic.Agent.Api.Experimental; using NewRelic.Agent.Extensions.Providers.Wrapper; using NewRelic.SystemExtensions; @@ -36,7 +37,9 @@ public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo) public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction) { var httpActionContext = instrumentedMethodCall.MethodCall.MethodArguments.ExtractNotNullAs(0); - var controllerName = TryGetControllerName(httpActionContext) ?? "Unknown Controller"; + var controllerDescriptor = TryGetControllerDescriptor(httpActionContext); + + var controllerName = controllerDescriptor?.ControllerName ?? "Unknown Controller"; var actionName = TryGetActionName(httpActionContext) ?? "Unknown Action"; var transactionName = string.Format("{0}/{1}", controllerName, actionName); @@ -44,20 +47,20 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins var segment = transaction.StartMethodSegment(instrumentedMethodCall.MethodCall, controllerName, actionName); + var segmentApi = segment.GetExperimentalApi(); + segmentApi.UserCodeNamespace = controllerDescriptor?.ControllerType.FullName; + segmentApi.UserCodeFunction = httpActionContext.ActionDescriptor?.ActionName; + return Delegates.GetAsyncDelegateFor>(agent, segment); } - private static string TryGetControllerName(HttpActionContext httpActionContext) + private static HttpControllerDescriptor TryGetControllerDescriptor(HttpActionContext httpActionContext) { var controllerContext = httpActionContext.ControllerContext; if (controllerContext == null) return null; - var controllerDescriptor = controllerContext.ControllerDescriptor; - if (controllerDescriptor == null) - return null; - - return controllerDescriptor.ControllerName; + return controllerContext.ControllerDescriptor; } private static string TryGetActionName(HttpActionContext httpActionContext) diff --git a/tests/Agent/IntegrationTests/Applications/AgentApiExecutor/Program.cs b/tests/Agent/IntegrationTests/Applications/AgentApiExecutor/Program.cs index b1cb20a20e..249a373ad8 100644 --- a/tests/Agent/IntegrationTests/Applications/AgentApiExecutor/Program.cs +++ b/tests/Agent/IntegrationTests/Applications/AgentApiExecutor/Program.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.IO; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading; using CommandLine; @@ -50,8 +51,10 @@ static void RealMain(string[] args) }; Api.Agent.NewRelic.NoticeError(new Exception("Rawr!"), errorAttributes); + SomeOtherMethod(); } + [MethodImpl(MethodImplOptions.NoInlining)] private static void SomeSlowMethod() { var stuff = string.Empty; @@ -59,6 +62,12 @@ private static void SomeSlowMethod() Thread.Sleep(2000); //needed for OtherTransaction test } + [MethodImpl(MethodImplOptions.NoInlining)] + private static void SomeOtherMethod() + { + Thread.Sleep(20); + } + private static void CreatePidFile() { var pid = Process.GetCurrentProcess().Id; diff --git a/tests/Agent/IntegrationTests/IntegrationTestHelpers/NewRelicConfigModifier.cs b/tests/Agent/IntegrationTests/IntegrationTestHelpers/NewRelicConfigModifier.cs index f50a2d7c7c..feaa54d336 100644 --- a/tests/Agent/IntegrationTests/IntegrationTestHelpers/NewRelicConfigModifier.cs +++ b/tests/Agent/IntegrationTests/IntegrationTestHelpers/NewRelicConfigModifier.cs @@ -314,5 +314,12 @@ public NewRelicConfigModifier SetLogForwardingMaxSamplesStored(int samples) CommonUtils.ModifyOrCreateXmlAttributeInNewRelicConfig(_configFilePath, new[] { "configuration", "applicationLogging", "forwarding" }, "maxSamplesStored", samples.ToString()); return this; } + + public NewRelicConfigModifier SetCodeLevelMetricsEnabled(bool enabled = true) + { + CommonUtils.ModifyOrCreateXmlNodeInNewRelicConfig(_configFilePath, new[] { "configuration" }, "codeLevelMetrics", string.Empty); + CommonUtils.ModifyOrCreateXmlAttributeInNewRelicConfig(_configFilePath, new[] { "configuration", "codeLevelMetrics" }, "enabled", enabled.ToString().ToLower()); + return this; + } } } diff --git a/tests/Agent/IntegrationTests/IntegrationTests/CodeLevelMetrics/AspNetCoreMvcCodeAttributeTests.cs b/tests/Agent/IntegrationTests/IntegrationTests/CodeLevelMetrics/AspNetCoreMvcCodeAttributeTests.cs new file mode 100644 index 0000000000..4dc1a19f0c --- /dev/null +++ b/tests/Agent/IntegrationTests/IntegrationTests/CodeLevelMetrics/AspNetCoreMvcCodeAttributeTests.cs @@ -0,0 +1,84 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + + +using System; +using System.Collections.Generic; +using System.Linq; +using NewRelic.Agent.IntegrationTestHelpers; +using NewRelic.Agent.IntegrationTestHelpers.Models; +using NewRelic.Testing.Assertions; +using Xunit; +using Xunit.Abstractions; + +namespace NewRelic.Agent.IntegrationTests.CodeLevelMetrics +{ + [NetCoreTest] + public class AspNetCoreMvcCodeAttributeTests : NewRelicIntegrationTest + { + private readonly RemoteServiceFixtures.AspNetCoreMvcBasicRequestsFixture _fixture; + + public AspNetCoreMvcCodeAttributeTests(RemoteServiceFixtures.AspNetCoreMvcBasicRequestsFixture fixture, ITestOutputHelper output) + : base(fixture) + { + _fixture = fixture; + _fixture.TestLogger = output; + _fixture.Actions + ( + setupConfiguration: () => + { + var configModifier = new NewRelicConfigModifier(_fixture.DestinationNewRelicConfigFilePath); + configModifier.SetCodeLevelMetricsEnabled(); + }, + exerciseApplication: () => + { + _fixture.Get(); + _fixture.ThrowException(); + _fixture.GetCallAsyncExternal(); + + _fixture.AgentLog.WaitForLogLine(AgentLogBase.SpanEventDataLogLineRegex, TimeSpan.FromMinutes(2)); + } + ); + _fixture.Initialize(); + } + + [Fact] + public void Test() + { + var spanEvents = _fixture.AgentLog.GetSpanEvents().ToList(); + + var getIndexSpan = spanEvents.FirstOrDefault(se => se.IntrinsicAttributes["name"].ToString() == "DotNet/HomeController/Index"); + var throwExceptionSpan = spanEvents.FirstOrDefault(se => se.IntrinsicAttributes["name"].ToString() == "DotNet/HomeController/ThrowException"); + var callAsyncExternalSpan = spanEvents.FirstOrDefault(se => se.IntrinsicAttributes["name"].ToString() == "DotNet/DetachWrapperController/CallAsyncExternal"); + + Assert.NotNull(getIndexSpan); + Assert.NotNull(throwExceptionSpan); + Assert.NotNull(callAsyncExternalSpan); + + NrAssert.Multiple + ( + () => Assertions.SpanEventHasAttributes(_expectedGetIndexAttributes, SpanEventAttributeType.Agent, getIndexSpan), + () => Assertions.SpanEventHasAttributes(_expectedThrowExceptionAttributes, SpanEventAttributeType.Agent, throwExceptionSpan), + () => Assertions.SpanEventHasAttributes(_expectedCallAsyncExternalAttributes, SpanEventAttributeType.Agent, callAsyncExternalSpan) + ); + } + + private readonly Dictionary _expectedGetIndexAttributes = new Dictionary() + { + { "code.namespace", "AspNetCoreMvcBasicRequestsApplication.Controllers.HomeController" }, + { "code.function", "Index" } + }; + + private readonly Dictionary _expectedThrowExceptionAttributes = new Dictionary() + { + { "code.namespace", "AspNetCoreMvcBasicRequestsApplication.Controllers.HomeController" }, + { "code.function", "ThrowException" } + }; + + private readonly Dictionary _expectedCallAsyncExternalAttributes = new Dictionary() + { + { "code.namespace", "AspNetCoreMvcBasicRequestsApplication.Controllers.DetachWrapperController" }, + { "code.function", "CallAsyncExternal" } + }; + } +} diff --git a/tests/Agent/IntegrationTests/IntegrationTests/CodeLevelMetrics/FrameworkAspNetMvcCodeAttributeTests.cs b/tests/Agent/IntegrationTests/IntegrationTests/CodeLevelMetrics/FrameworkAspNetMvcCodeAttributeTests.cs new file mode 100644 index 0000000000..abea14eca2 --- /dev/null +++ b/tests/Agent/IntegrationTests/IntegrationTests/CodeLevelMetrics/FrameworkAspNetMvcCodeAttributeTests.cs @@ -0,0 +1,76 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Linq; +using NewRelic.Agent.IntegrationTestHelpers; +using NewRelic.Agent.IntegrationTestHelpers.Models; +using Xunit; +using Xunit.Abstractions; + +namespace NewRelic.Agent.IntegrationTests.CodeLevelMetrics +{ + [NetFrameworkTest] + public class FrameworkAspNetMvcCodeAttributeTests : NewRelicIntegrationTest + { + + private readonly RemoteServiceFixtures.BasicMvcApplicationTestFixture _fixture; + + public FrameworkAspNetMvcCodeAttributeTests(RemoteServiceFixtures.BasicMvcApplicationTestFixture fixture, ITestOutputHelper output) + : base(fixture) + { + _fixture = fixture; + _fixture.TestLogger = output; + _fixture.Actions + ( + setupConfiguration: () => + { + var configPath = fixture.DestinationNewRelicConfigFilePath; + var configModifier = new NewRelicConfigModifier(configPath); + configModifier.SetCodeLevelMetricsEnabled(); + }, + exerciseApplication: () => + { + _fixture.Get(); + _fixture.GetWithAsyncDisabled(); + + _fixture.AgentLog.WaitForLogLine(AgentLogBase.SpanEventDataLogLineRegex, TimeSpan.FromMinutes(2)); + } + ); + _fixture.Initialize(); + } + + [Fact] + public void AsyncDisabledControllerTest() + { + var spanEvents = _fixture.AgentLog.GetSpanEvents().ToList(); + + const string spanName = "DotNet/DisableAsyncSupportController/Index"; + Assert.Contains(spanEvents, x => x.IntrinsicAttributes["name"].ToString() == spanName); + var spanEvent = spanEvents.FirstOrDefault(x => x.IntrinsicAttributes["name"].ToString() == spanName); + Assert.NotNull(spanEvent); + + Assertions.SpanEventHasAttributes(new Dictionary{ + { "code.namespace", "BasicMvcApplication.Controllers.DisableAsyncSupportController" }, + { "code.function", "Index" } + }, SpanEventAttributeType.Agent, spanEvent); + } + + [Fact] + public void DefaultControllerTest() + { + var spanEvents = _fixture.AgentLog.GetSpanEvents().ToList(); + + const string spanName = "DotNet/DefaultController/Index"; + Assert.Contains(spanEvents, x => x.IntrinsicAttributes["name"].ToString() == spanName); + var spanEvent = spanEvents.FirstOrDefault(x => x.IntrinsicAttributes["name"].ToString() == spanName); + Assert.NotNull(spanEvent); + + Assertions.SpanEventHasAttributes(new Dictionary{ + { "code.namespace", "BasicMvcApplication.Controllers.DefaultController" }, + { "code.function", "Index" } + }, SpanEventAttributeType.Agent, spanEvent); + } + } +} diff --git a/tests/Agent/IntegrationTests/IntegrationTests/CodeLevelMetrics/FrameworkCustomInstrumentationCodeAttributeTests.cs b/tests/Agent/IntegrationTests/IntegrationTests/CodeLevelMetrics/FrameworkCustomInstrumentationCodeAttributeTests.cs new file mode 100644 index 0000000000..0fbaae6f70 --- /dev/null +++ b/tests/Agent/IntegrationTests/IntegrationTests/CodeLevelMetrics/FrameworkCustomInstrumentationCodeAttributeTests.cs @@ -0,0 +1,88 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NewRelic.Agent.IntegrationTestHelpers; +using NewRelic.Agent.IntegrationTestHelpers.Models; +using NewRelic.Testing.Assertions; +using Xunit; +using Xunit.Abstractions; + +namespace NewRelic.Agent.IntegrationTests.CodeLevelMetrics +{ + [NetFrameworkTest] + public class FrameworkCustomInstrumentationCodeAttributeTests : NewRelicIntegrationTest + { + private readonly RemoteServiceFixtures.AgentApiExecutor _fixture; + + private const string ProgramNamespace = "NewRelic.Agent.IntegrationTests.Applications.AgentApiExecutor.Program"; + + public FrameworkCustomInstrumentationCodeAttributeTests(RemoteServiceFixtures.AgentApiExecutor fixture, ITestOutputHelper output) + : base(fixture) + { + _fixture = fixture; + _fixture.TestLogger = output; + _fixture.Actions + ( + setupConfiguration: () => + { + var configModifier = new NewRelicConfigModifier(_fixture.DestinationNewRelicConfigFilePath); + configModifier.SetCodeLevelMetricsEnabled(); + + var instrumentationFilePath = Path.Combine(fixture.DestinationNewRelicExtensionsDirectoryPath, "CustomInstrumentation.xml"); + + CommonUtils.AddCustomInstrumentation(instrumentationFilePath, "NewRelic.Agent.IntegrationTests.Applications.AgentApiExecutor", ProgramNamespace, "RealMain", "NewRelic.Providers.Wrapper.CustomInstrumentation.OtherTransactionWrapper", "MyCustomMetricName"); + CommonUtils.AddCustomInstrumentation(instrumentationFilePath, "NewRelic.Agent.IntegrationTests.Applications.AgentApiExecutor", ProgramNamespace, "SomeSlowMethod", "NewRelic.Agent.Core.Tracer.Factories.BackgroundThreadTracerFactory"); + + // Use the default wrapper + CommonUtils.AddCustomInstrumentation(instrumentationFilePath, "NewRelic.Agent.IntegrationTests.Applications.AgentApiExecutor", ProgramNamespace, "SomeOtherMethod"); + } + ); + _fixture.Initialize(); + } + + [Fact] + public void Test() + { + var spanEvents = _fixture.AgentLog.GetSpanEvents().ToList(); + + var myCustomMetricNameSpan = spanEvents.FirstOrDefault(se => se.IntrinsicAttributes["name"].ToString() == "MyCustomMetricName"); + var someSlowMethodSpan = spanEvents.FirstOrDefault(se => se.IntrinsicAttributes["name"].ToString().Contains("SomeSlowMethod")); + var someOtherMethodSpan = spanEvents.FirstOrDefault(se => se.IntrinsicAttributes["name"].ToString().Contains("SomeOtherMethod")); + + Assert.NotNull(myCustomMetricNameSpan); + Assert.NotNull(someSlowMethodSpan); + Assert.NotNull(someOtherMethodSpan); + + NrAssert.Multiple + ( + () => Assertions.SpanEventHasAttributes(_expectedMyCustomMetricNameAttributes, SpanEventAttributeType.Agent, myCustomMetricNameSpan), + () => Assertions.SpanEventHasAttributes(_expectedSomeSlowMethodAttributes, SpanEventAttributeType.Agent, someSlowMethodSpan), + () => Assertions.SpanEventHasAttributes(_expectedSomeOtherMethodAttributes, SpanEventAttributeType.Agent, someOtherMethodSpan) + ); + } + + private readonly Dictionary _expectedMyCustomMetricNameAttributes = new Dictionary() + { + { "code.namespace", ProgramNamespace }, + { "code.function", "RealMain" } + }; + + private readonly Dictionary _expectedSomeSlowMethodAttributes = new Dictionary() + { + { "code.namespace", ProgramNamespace }, + { "code.function", "SomeSlowMethod" } + }; + + private readonly Dictionary _expectedSomeOtherMethodAttributes = new Dictionary() + { + { "code.namespace", ProgramNamespace }, + { "code.function", "SomeOtherMethod" } + }; + + } +} diff --git a/tests/Agent/IntegrationTests/IntegrationTests/CodeLevelMetrics/NetCoreCustomInstrumentationCodeAttributeTests.cs b/tests/Agent/IntegrationTests/IntegrationTests/CodeLevelMetrics/NetCoreCustomInstrumentationCodeAttributeTests.cs new file mode 100644 index 0000000000..35a679098d --- /dev/null +++ b/tests/Agent/IntegrationTests/IntegrationTests/CodeLevelMetrics/NetCoreCustomInstrumentationCodeAttributeTests.cs @@ -0,0 +1,90 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NewRelic.Agent.IntegrationTestHelpers; +using NewRelic.Agent.IntegrationTests.RemoteServiceFixtures; +using NewRelic.Agent.IntegrationTestHelpers.Models; +using NewRelic.Testing.Assertions; +using Xunit; +using Xunit.Abstractions; +using System; + +namespace NewRelic.Agent.IntegrationTests.CodeLevelMetrics +{ + [NetCoreTest] + public class NetCoreCustomInstrumentationCodeAttributeTests : NewRelicIntegrationTest + { + private readonly NetCoreAsyncTestsFixture _fixture; + + private const string AsyncUseCasesNamespace = "NetCoreAsyncApplication.AsyncUseCases"; + + public NetCoreCustomInstrumentationCodeAttributeTests(NetCoreAsyncTestsFixture fixture, ITestOutputHelper output) : base(fixture) + { + _fixture = fixture; + _fixture.TestLogger = output; + _fixture.Actions + ( + setupConfiguration: () => + { + var configModifier = new NewRelicConfigModifier(_fixture.DestinationNewRelicConfigFilePath); + configModifier.SetCodeLevelMetricsEnabled(); + + var instrumentationFilePath = Path.Combine(fixture.DestinationNewRelicExtensionsDirectoryPath, "CustomInstrumentation.xml"); + + CommonUtils.AddCustomInstrumentation(instrumentationFilePath, "NetCoreAsyncApplication", AsyncUseCasesNamespace, "IoBoundNoSpecialAsync", "NewRelic.Providers.Wrapper.CustomInstrumentationAsync.OtherTransactionWrapperAsync", "IoBoundNoSpecialAsync", 7); + CommonUtils.AddCustomInstrumentation(instrumentationFilePath, "NetCoreAsyncApplication", AsyncUseCasesNamespace, "IoBoundConfigureAwaitFalseAsync", "NewRelic.Providers.Wrapper.CustomInstrumentationAsync.OtherTransactionWrapperAsync", "IoBoundConfigureAwaitFalseAsync", 7); + + CommonUtils.AddCustomInstrumentation(instrumentationFilePath, "NetCoreAsyncApplication", AsyncUseCasesNamespace, "CustomMethodAsync1", "NewRelic.Providers.Wrapper.CustomInstrumentationAsync.DefaultWrapperAsync", "CustomMethodAsync1"); + }, + exerciseApplication: () => + { + _fixture.AgentLog.WaitForLogLine(AgentLogBase.SpanEventDataLogLineRegex, TimeSpan.FromMinutes(2)); + } + ); + _fixture.Initialize(); + } + + [Fact] + public void Test() + { + var spanEvents = _fixture.AgentLog.GetSpanEvents().ToList(); + + var ioBoundNoSpecialSpan = spanEvents.FirstOrDefault(se => se.IntrinsicAttributes["name"].ToString() == "IoBoundNoSpecialAsync"); + var ioBoundConfigureAwaitFalseSpan = spanEvents.FirstOrDefault(se => se.IntrinsicAttributes["name"].ToString() == "IoBoundConfigureAwaitFalseAsync"); + var customMethodAsync1Span = spanEvents.FirstOrDefault(se => se.IntrinsicAttributes["name"].ToString() == "CustomMethodAsync1"); + + Assert.NotNull(ioBoundNoSpecialSpan); + Assert.NotNull(ioBoundConfigureAwaitFalseSpan); + Assert.NotNull(customMethodAsync1Span); + + NrAssert.Multiple + ( + () => Assertions.SpanEventHasAttributes(_expectedIoBoundNoSpecialAttributes, SpanEventAttributeType.Agent, ioBoundNoSpecialSpan), + () => Assertions.SpanEventHasAttributes(_expectedIoBoundConfigureAwaitFalseAttributes, SpanEventAttributeType.Agent, ioBoundConfigureAwaitFalseSpan), + () => Assertions.SpanEventHasAttributes(_expectedCustomMethodAsync1Attributes, SpanEventAttributeType.Agent, customMethodAsync1Span) + ); + } + + private readonly Dictionary _expectedIoBoundNoSpecialAttributes = new Dictionary() + { + { "code.namespace", AsyncUseCasesNamespace }, + { "code.function", "IoBoundNoSpecialAsync" } + }; + + private readonly Dictionary _expectedIoBoundConfigureAwaitFalseAttributes = new Dictionary() + { + { "code.namespace", AsyncUseCasesNamespace }, + { "code.function", "IoBoundConfigureAwaitFalseAsync" } + }; + + private readonly Dictionary _expectedCustomMethodAsync1Attributes = new Dictionary() + { + { "code.namespace", AsyncUseCasesNamespace }, + { "code.function", "CustomMethodAsync1" } + }; + } +} diff --git a/tests/Agent/IntegrationTests/IntegrationTests/CodeLevelMetrics/OwinWebApi2CodeAttributeTests.cs b/tests/Agent/IntegrationTests/IntegrationTests/CodeLevelMetrics/OwinWebApi2CodeAttributeTests.cs new file mode 100644 index 0000000000..bd8568e5f7 --- /dev/null +++ b/tests/Agent/IntegrationTests/IntegrationTests/CodeLevelMetrics/OwinWebApi2CodeAttributeTests.cs @@ -0,0 +1,109 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; +using Xunit.Abstractions; +using NewRelic.Agent.IntegrationTestHelpers; +using NewRelic.Agent.IntegrationTestHelpers.Models; +using NewRelic.Testing.Assertions; + +namespace NewRelic.Agent.IntegrationTests.CodeLevelMetrics +{ + [NetFrameworkTest] + public abstract class OwinWebApi2CodeAttributeTestsBase : NewRelicIntegrationTest + where TFixture : RemoteServiceFixtures.OwinWebApiFixture + { + private readonly RemoteServiceFixtures.OwinWebApiFixture _fixture; + + // The base test class runs tests for Owin 2; the derived classes test Owin 3 and 4 + protected OwinWebApi2CodeAttributeTestsBase(TFixture fixture, ITestOutputHelper output) : base(fixture) + { + _fixture = fixture; + _fixture.TestLogger = output; + _fixture.Actions + ( + setupConfiguration: () => + { + var configModifier = new NewRelicConfigModifier(_fixture.DestinationNewRelicConfigFilePath); + configModifier.SetCodeLevelMetricsEnabled(); + }, + exerciseApplication: () => + { + _fixture.Get(); + _fixture.Get404(); + _fixture.Post(); + + _fixture.AgentLog.WaitForLogLine(AgentLogBase.SpanEventDataLogLineRegex, TimeSpan.FromMinutes(2)); + } + ); + _fixture.Initialize(); + } + + [Fact] + public void Test() + { + var expectedNamespace = $"{_fixture.AssemblyName}.Controllers.ValuesController"; + + var expectedValuesGetAttributes = GetExpectedAttributesDefinition(expectedNamespace, "Get"); + var expectedValuesPostAttributes = GetExpectedAttributesDefinition(expectedNamespace, "Post"); + var expectedValuesGet404Attributes = GetExpectedAttributesDefinition(expectedNamespace, "Get404"); + + var spanEvents = _fixture.AgentLog.GetSpanEvents().ToList(); + + var valuesGetSpan = spanEvents.FirstOrDefault(se => se.IntrinsicAttributes["name"].ToString() == "DotNet/Values/Get"); + var valuesPostSpan = spanEvents.FirstOrDefault(se => se.IntrinsicAttributes["name"].ToString() == "DotNet/Values/Post"); + var valuesGet404Span = spanEvents.FirstOrDefault(se => se.IntrinsicAttributes["name"].ToString() == "DotNet/Values/Get404"); + + Assert.NotNull(valuesGetSpan); + Assert.NotNull(valuesPostSpan); + Assert.NotNull(valuesGet404Span); + + NrAssert.Multiple + ( + () => Assertions.SpanEventHasAttributes(expectedValuesGetAttributes, SpanEventAttributeType.Agent, valuesGetSpan), + () => Assertions.SpanEventHasAttributes(expectedValuesPostAttributes, SpanEventAttributeType.Agent, valuesPostSpan), + () => Assertions.SpanEventHasAttributes(expectedValuesGet404Attributes, SpanEventAttributeType.Agent, valuesGet404Span) + ); + } + + private Dictionary GetExpectedAttributesDefinition(string fullType, string methodName) + { + var expectedAttributes = new Dictionary() + { + { "code.namespace", fullType }, + { "code.function", methodName } + }; + + return expectedAttributes; + } + } + + public class OwinWebApi2CodeAttributeTests : OwinWebApi2CodeAttributeTestsBase + { + public OwinWebApi2CodeAttributeTests(RemoteServiceFixtures.OwinWebApiFixture fixture, ITestOutputHelper output) + : base(fixture, output) + { + } + } + + public class Owin3WebApi2CodeAttributeTests : OwinWebApi2CodeAttributeTestsBase + { + public Owin3WebApi2CodeAttributeTests(RemoteServiceFixtures.Owin3WebApiFixture fixture, ITestOutputHelper output) + : base(fixture, output) + { + } + } + + public class Owin4WebApi2CodeAttributeTests : OwinWebApi2CodeAttributeTestsBase + { + public Owin4WebApi2CodeAttributeTests(RemoteServiceFixtures.Owin4WebApiFixture fixture, ITestOutputHelper output) + : base(fixture, output) + { + } + } + +} diff --git a/tests/Agent/IntegrationTests/IntegrationTests/CodeLevelMetrics/WebApi2CodeAttributeTests.cs b/tests/Agent/IntegrationTests/IntegrationTests/CodeLevelMetrics/WebApi2CodeAttributeTests.cs new file mode 100644 index 0000000000..ec086f7c9f --- /dev/null +++ b/tests/Agent/IntegrationTests/IntegrationTests/CodeLevelMetrics/WebApi2CodeAttributeTests.cs @@ -0,0 +1,88 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + + +using System; +using System.Collections.Generic; +using System.Linq; +using NewRelic.Agent.IntegrationTestHelpers; +using NewRelic.Agent.IntegrationTestHelpers.Models; +using NewRelic.Agent.IntegrationTests.RemoteServiceFixtures; +using NewRelic.Testing.Assertions; +using Xunit; +using Xunit.Abstractions; + +namespace NewRelic.Agent.IntegrationTests.CodeLevelMetrics +{ + [NetFrameworkTest] + public class WebApi2CodeAttributeTests : NewRelicIntegrationTest + { + private readonly WebApiAsyncFixture _fixture; + + private const string AsyncAwaitControllerNamespace = "WebApiAsyncApplication.Controllers.AsyncAwaitController"; + + public WebApi2CodeAttributeTests(WebApiAsyncFixture fixture, ITestOutputHelper output) : base(fixture) + { + _fixture = fixture; + _fixture.TestLogger = output; + _fixture.Actions + ( + setupConfiguration: () => + { + var configModifier = new NewRelicConfigModifier(_fixture.DestinationNewRelicConfigFilePath); + configModifier.SetCodeLevelMetricsEnabled(); + }, + exerciseApplication: () => + { + _fixture.GetIoBoundNoSpecialAsync(); + _fixture.GetIoBoundConfigureAwaitFalseAsync(); + _fixture.GetCpuBoundTasksAsync(); + + _fixture.AgentLog.WaitForLogLine(AgentLogBase.SpanEventDataLogLineRegex, TimeSpan.FromMinutes(2)); + } + ); + _fixture.Initialize(); + } + + [Fact] + public void Test() + { + var spanEvents = _fixture.AgentLog.GetSpanEvents().ToList(); + + var getIoBoundNoSpecialSpan = spanEvents.FirstOrDefault(se => se.IntrinsicAttributes["name"].ToString() == "DotNet/AsyncAwait/IoBoundNoSpecialAsync"); + var getIoBoundConfigureAwaitFalseSpan = spanEvents.FirstOrDefault(se => se.IntrinsicAttributes["name"].ToString() == "DotNet/AsyncAwait/IoBoundConfigureAwaitFalseAsync"); + var getCpuBoundSpan = spanEvents.FirstOrDefault(se => se.IntrinsicAttributes["name"].ToString() == "DotNet/AsyncAwait/CpuBoundTasksAsync"); + + Assert.NotNull(getIoBoundNoSpecialSpan); + Assert.NotNull(getIoBoundConfigureAwaitFalseSpan); + Assert.NotNull(getCpuBoundSpan); + + NrAssert.Multiple + ( + () => Assertions.SpanEventHasAttributes(_expectedGetIoBoundNoSpecialAsyncAttributes, SpanEventAttributeType.Agent, getIoBoundNoSpecialSpan), + () => Assertions.SpanEventHasAttributes(_expectedGetIoBoundConfigureAwaitFalseAsyncAttributes, SpanEventAttributeType.Agent, getIoBoundConfigureAwaitFalseSpan), + () => Assertions.SpanEventHasAttributes(_expectedGetCpuBoundTasksAsyncAttributes, SpanEventAttributeType.Agent, getCpuBoundSpan) + ); + } + + private readonly Dictionary _expectedGetIoBoundNoSpecialAsyncAttributes = new Dictionary() + { + { "code.namespace", AsyncAwaitControllerNamespace }, + { "code.function", "IoBoundNoSpecialAsync" } + }; + + private readonly Dictionary _expectedGetIoBoundConfigureAwaitFalseAsyncAttributes = new Dictionary() + { + { "code.namespace", AsyncAwaitControllerNamespace }, + { "code.function", "IoBoundConfigureAwaitFalseAsync" } + }; + + private readonly Dictionary _expectedGetCpuBoundTasksAsyncAttributes = new Dictionary() + { + { "code.namespace", AsyncAwaitControllerNamespace }, + { "code.function", "CpuBoundTasksAsync" } + }; + + + } +} diff --git a/tests/Agent/UnitTests/Core.UnitTest/Configuration/DefaultConfigurationTests.cs b/tests/Agent/UnitTests/Core.UnitTest/Configuration/DefaultConfigurationTests.cs index ffd5abfcd0..76d0d5b817 100644 --- a/tests/Agent/UnitTests/Core.UnitTest/Configuration/DefaultConfigurationTests.cs +++ b/tests/Agent/UnitTests/Core.UnitTest/Configuration/DefaultConfigurationTests.cs @@ -3099,6 +3099,39 @@ public bool GloballyForceNewTransactionConfigurationTests(string environmentSett return _defaultConfig.ForceNewTransactionOnNewThread; } + [Test] + public void ShouldDefaultCodeLevelMetricsEnabledFalse() + { + Assert.AreEqual(false, _defaultConfig.CodeLevelMetricsEnabled); + } + + [TestCase(true, null, ExpectedResult = true)] + [TestCase(true, "true", ExpectedResult = true)] + [TestCase(true, "1", ExpectedResult = true)] + [TestCase(true, "false", ExpectedResult = false)] + [TestCase(true, "0", ExpectedResult = false)] + [TestCase(true, "invalid", ExpectedResult = true)] + [TestCase(false, null, ExpectedResult = false)] + [TestCase(false, "true", ExpectedResult = true)] + [TestCase(false, "1", ExpectedResult = true)] + [TestCase(false, "false", ExpectedResult = false)] + [TestCase(false, "0", ExpectedResult = false)] + [TestCase(false, "invalid", ExpectedResult = false)] + [TestCase(null, "true", ExpectedResult = true)] + [TestCase(null, "1", ExpectedResult = true)] + [TestCase(null, "false", ExpectedResult = false)] + [TestCase(null, "0", ExpectedResult = false)] + [TestCase(null, "invalid", ExpectedResult = false)] + public bool ShouldUpdateCodeLevelMetricsEnabled(bool localConfigValue, string envConfigValue) + { + Mock.Arrange(() => _environment.GetEnvironmentVariable("NEW_RELIC_CODE_LEVEL_METRICS_ENABLED")).Returns(envConfigValue); + + _localConfig.codeLevelMetrics.enabled = localConfigValue; + + return _defaultConfig.CodeLevelMetricsEnabled; + } + + private void CreateDefaultConfiguration() { _defaultConfig = new TestableDefaultConfiguration(_environment, _localConfig, _serverConfig, _runTimeConfig, _securityPoliciesConfiguration, _processStatic, _httpRuntimeStatic, _configurationManagerStatic, _dnsStatic);