Skip to content

Commit ca4fe5b

Browse files
committed
feat: Add hybrid agent support for grpc-dotnet.
1 parent 57e001a commit ca4fe5b

21 files changed

Lines changed: 870 additions & 79 deletions

File tree

src/Agent/NewRelic/Agent/Core/Attributes/AttributeDefinitionService.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,11 @@ public interface IAttributeDefinitions
6262
AttributeDefinition<string, string> Guid { get; }
6363
AttributeDefinition<string, string> HostDisplayName { get; }
6464
AttributeDefinition<string, string> HttpMethod { get; }
65+
AttributeDefinition<string, string> HttpRequestMethod { get; }
6566
AttributeDefinition<long?, long> HttpStatusCode { get; }
6667
AttributeDefinition<string, string> HttpStatusText { get; }
6768
AttributeDefinition<Uri, string> HttpUrl { get; }
69+
AttributeDefinition<long?, long> GrpcStatusCode { get; }
6870
AttributeDefinition<bool, bool> IsError { get; }
6971
AttributeDefinition<string, string> NameForSpan { get; }
7072
AttributeDefinition<bool, bool> NrEntryPoint { get; }
@@ -475,6 +477,18 @@ public AttributeDefinition<TypeAttributeValue, string> GetTypeAttribute(TypeAttr
475477
.AppliesTo(AttributeDestinations.TransactionTrace)
476478
.Build(_attribFilter);
477479

480+
private AttributeDefinition<long?, long> _grpcStatusCode;
481+
public AttributeDefinition<long?, long> GrpcStatusCode => _grpcStatusCode ?? (_grpcStatusCode =
482+
AttributeDefinitionBuilder.CreateLong<long?>("grpc.statusCode", AttributeClassification.AgentAttributes)
483+
.AppliesTo(AttributeDestinations.ErrorEvent)
484+
.AppliesTo(AttributeDestinations.ErrorTrace)
485+
.AppliesTo(AttributeDestinations.TransactionEvent)
486+
.AppliesTo(AttributeDestinations.ErrorEvent)
487+
.AppliesTo(AttributeDestinations.SpanEvent)
488+
.AppliesTo(AttributeDestinations.TransactionTrace)
489+
.WithConvert(x => x.GetValueOrDefault()) //This is ok b/c we check for null input earlier
490+
.Build(_attribFilter));
491+
478492
private AttributeDefinition<string, string> _clientCrossProcessId;
479493
public AttributeDefinition<string, string> ClientCrossProcessId => _clientCrossProcessId ?? (_clientCrossProcessId =
480494
AttributeDefinitionBuilder.CreateString("client_cross_process_id", AttributeClassification.Intrinsics)
@@ -669,6 +683,12 @@ public AttributeDefinition<TypeAttributeValue, string> GetTypeAttribute(TypeAttr
669683

670684
private AttributeDefinition<string, string> _httpMethod;
671685
public AttributeDefinition<string, string> HttpMethod => _httpMethod ?? (_httpMethod =
686+
AttributeDefinitionBuilder.CreateString("http.method", AttributeClassification.AgentAttributes)
687+
.AppliesTo(AttributeDestinations.SpanEvent)
688+
.Build(_attribFilter));
689+
690+
private AttributeDefinition<string, string> _httpRequestMethod;
691+
public AttributeDefinition<string, string> HttpRequestMethod => _httpRequestMethod ?? (_httpRequestMethod =
672692
AttributeDefinitionBuilder.CreateString("http.request.method", AttributeClassification.AgentAttributes)
673693
.AppliesTo(AttributeDestinations.SpanEvent)
674694
.Build(_attribFilter));

src/Agent/NewRelic/Agent/Core/OpenTelemetryBridge/Tracing/ActivityBridgeSegmentHelpers.cs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ public static void ProcessActivityTags(this ISegment segment, object originalAct
5858
{
5959
ProcessRpcClientTags(segment, agent, errorService, tags, activityLogPrefix, rpcSystem);
6060
}
61+
else if (tags.TryGetTag<string>(["grpc.method"], out _)) // Its gRPC - likely grpc-dotnet
62+
{
63+
ProcessRpcClientTags(segment, agent, errorService, tags, activityLogPrefix, "grpc");
64+
}
6165
else if (tags.TryGetAndRemoveTag<string>(["http.request.method", "http.method"], out var method)) // it's an HTTP call
6266
{
6367
ProcessClientExternalTags(segment, agent, tags, activityLogPrefix, method);
@@ -136,25 +140,34 @@ private static void GetInstrumentationScopeAttributes(ISegment segment, dynamic
136140

137141
private static void ProcessRpcClientTags(ISegment segment, IAgent agent, IErrorService errorService, Dictionary<string, object> tags, string activityLogPrefix, string rpcSystem)
138142
{
139-
tags.TryGetAndRemoveTag<int?>(["rpc.grpc.status_code"], out var statusCode);
143+
tags.TryGetAndRemoveTag<int?>(["rpc.grpc.status_code", "grpc.status_code"], out var statusCode);
140144

141145
tags.TryGetAndRemoveTag<string>(["rpc.method"], out var method);
142-
tags.TryGetAndRemoveTag<string>(["rpc.service"], out var service);
146+
tags.TryGetAndRemoveTag<string>(["rpc.service"], out var service); // deprecated attribute, rpc.method includes this information in the format /service/method, but some instrumentation may still use this tag so we'll look for it as a fallback
143147
tags.TryGetAndRemoveTag<string>(["grpc.method"], out var grpcMethod);
148+
var cleanGrpcMethod = grpcMethod?.TrimStart('/'); // per spec, we need to strip leading slashes.
144149

145150
tags.TryGetAndRemoveTag<string>(["server.address", "network.peer.address"], out var host);
151+
152+
if (string.IsNullOrWhiteSpace(host))
153+
{
154+
host = ((Segment)segment).GetCacheItem("server.address") as string;
155+
}
156+
146157
tags.TryGetAndRemoveTag<int?>(["server.port", "network.peer.port"], out var port);
158+
port ??= ((Segment)segment).GetCacheItem("server.port") as int?;
147159

148-
var path = BuildRpcPath(host ?? "unknown", port ?? 0, service, method, grpcMethod);
160+
var path = BuildRpcPath(host ?? "unknown", port ?? 0, service, method, cleanGrpcMethod);
149161
Uri uri = new Uri(path);
150-
var externalSegmentData = new ExternalSegmentData(uri, method, componentOverride: rpcSystem);
162+
var externalSegmentData = new ExternalGrpcSegmentData(uri: uri, method: method ?? cleanGrpcMethod, componentOverride: rpcSystem);
151163

152164
if (statusCode.HasValue)
153-
externalSegmentData.SetHttpStatus(statusCode.Value);
165+
externalSegmentData.SetGrpcStatus(statusCode.Value);
154166

155-
Log.Finest($"Created ExternalSegmentData for {activityLogPrefix}.");
167+
Log.Finest($"{activityLogPrefix} Created ExternalGrpcSegmentData.");
156168

157-
segment.GetExperimentalApi().SetSegmentData(externalSegmentData);
169+
segment.GetExperimentalApi().SetSegmentData(externalSegmentData)
170+
.MakeLeaf();
158171

159172
// per spec, a non-zero status code must be recorded as an exception.
160173
// TODO: This behavior is supposed to be configurable by the customer but currently is not
@@ -203,7 +216,7 @@ private static string BuildRpcPath(string host, int? port, string service, strin
203216
{
204217
// construct the path according to gRPC spec
205218
// if service is missing, grpcMethod is the full path
206-
return !string.IsNullOrEmpty(service) ? $"grpc://{host}:{port}/{service}/{method}" : $"grpc://{host}:{port}{grpcMethod}";
219+
return !string.IsNullOrEmpty(service) ? $"grpc://{host}:{port}/{service}/{method}" : $"grpc://{host}:{port}/{grpcMethod}";
207220
}
208221

209222
private static void RecordGrpcException(ISegment segment, IAgent agent, IErrorService errorService, int statusCode, string path, string activityLogPrefix)

src/Agent/NewRelic/Agent/Core/Segments/ExternalSegmentData.cs

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,20 @@
1515

1616
namespace NewRelic.Agent.Core.Segments;
1717

18-
public class ExternalSegmentData : AbstractSegmentData, IExternalSegmentData
18+
public class ExternalSegmentData(Uri uri, string method, string library = "Stream", CrossApplicationResponseData crossApplicationResponseData = null, string componentOverride = null) : AbstractSegmentData, IExternalSegmentData
1919
{
2020
private const string TransactionGuidSegmentParameterKey = "transaction_guid";
2121

2222
private int? _httpStatusCode;
2323
private string _httpStatusText;
24-
private readonly string _componentOverride;
24+
private readonly string _componentOverride = componentOverride;
2525

2626
public override SpanCategory SpanCategory => SpanCategory.Http;
2727

28-
public Uri Uri { get; }
29-
public string Method { get; }
30-
public string Type { get; }
31-
32-
public ExternalSegmentData(Uri uri, string method, CrossApplicationResponseData crossApplicationResponseData = null, string componentOverride = null)
33-
{
34-
Uri = uri;
35-
Method = method;
36-
CrossApplicationResponseData = crossApplicationResponseData;
37-
_componentOverride = componentOverride;
38-
}
39-
40-
public CrossApplicationResponseData CrossApplicationResponseData { get; set; }
28+
public Uri Uri { get; } = uri;
29+
public string Method { get; } = method;
30+
31+
public CrossApplicationResponseData CrossApplicationResponseData { get; set; } = crossApplicationResponseData;
4132

4233
public void SetHttpStatus(int httpStatusCode, string httpStatusText = null)
4334
{
@@ -71,12 +62,12 @@ public override void AddMetricStats(Segment segment, TimeSpan durationOfChildren
7162
if (CrossApplicationResponseData == null)
7263
{
7364
// Generate scoped and unscoped external metrics as CAT not present.
74-
MetricBuilder.TryBuildExternalSegmentMetric(Uri.Host, Method, duration, exclusiveDuration, txStats, false);
65+
MetricBuilder.TryBuildExternalSegmentMetric(Uri.Host, Method, library, duration, exclusiveDuration, txStats, false);
7566
}
7667
else
7768
{
7869
// Only generate unscoped metric for response with CAT headers because segments should only produce a single scoped metric and the CAT metric is more interesting than the external segment metric.
79-
MetricBuilder.TryBuildExternalSegmentMetric(Uri.Host, Method, duration, exclusiveDuration, txStats, true);
70+
MetricBuilder.TryBuildExternalSegmentMetric(Uri.Host, Method, library, duration, exclusiveDuration, txStats, true);
8071

8172
var externalCrossProcessId = CrossApplicationResponseData.CrossProcessId;
8273
var externalTransactionName = CrossApplicationResponseData.TransactionName;
@@ -92,7 +83,7 @@ public override void SetSpanTypeSpecificAttributes(SpanAttributeValueCollection
9283
{
9384
AttribDefs.SpanCategory.TrySetValue(attribVals, SpanCategory.Http);
9485
AttribDefs.HttpUrl.TrySetValue(attribVals, Uri);
95-
AttribDefs.HttpMethod.TrySetValue(attribVals, Method);
86+
AttribDefs.HttpRequestMethod.TrySetValue(attribVals, Method);
9687
AttribDefs.Component.TrySetValue(attribVals, _componentOverride ?? _segmentState.TypeName);
9788
AttribDefs.SpanKind.TrySetDefault(attribVals);
9889
AttribDefs.HttpStatusCode.TrySetValue(attribVals, _httpStatusCode); //Attrib handles null
@@ -105,7 +96,7 @@ public override string GetTransactionTraceName()
10596
{
10697
// APM expects metric names to be used for external segment trace names
10798
var name = CrossApplicationResponseData == null
108-
? MetricNames.GetExternalHost(Uri.Host, "Stream", Method)
99+
? MetricNames.GetExternalHost(Uri.Host, library, Method)
109100
: MetricNames.GetExternalTransaction(Uri.Host, CrossApplicationResponseData.CrossProcessId, CrossApplicationResponseData.TransactionName);
110101
return name.ToString();
111102
}
@@ -125,3 +116,27 @@ public override bool IsCombinableWith(AbstractSegmentData otherData)
125116
return true;
126117
}
127118
}
119+
120+
public class ExternalGrpcSegmentData(Uri uri, string method, CrossApplicationResponseData crossApplicationResponseData = null, string componentOverride = null)
121+
: ExternalSegmentData(uri, method, "gRPC", crossApplicationResponseData)
122+
{
123+
private int? _grpcStatusCode;
124+
125+
public void SetGrpcStatus(int grpcStatusCode)
126+
{
127+
_grpcStatusCode = grpcStatusCode;
128+
}
129+
130+
public override void SetSpanTypeSpecificAttributes(SpanAttributeValueCollection attribVals)
131+
{
132+
AttribDefs.SpanCategory.TrySetValue(attribVals, SpanCategory.Http);
133+
AttribDefs.HttpUrl.TrySetValue(attribVals, Uri);
134+
AttribDefs.HttpMethod.TrySetValue(attribVals, Method); //TODO gRPC spec says this is the correct "method" attribute
135+
AttribDefs.HttpRequestMethod.TrySetValue(attribVals, Method); //TODO Otel spec says this should be "procedure", but externals use this
136+
AttribDefs.Component.TrySetValue(attribVals, componentOverride ?? _segmentState.TypeName);
137+
AttribDefs.SpanKind.TrySetDefault(attribVals);
138+
AttribDefs.ServerAddress.TrySetValue(attribVals, Uri.Host);
139+
AttribDefs.ServerPort.TrySetValue(attribVals, Uri.Port);
140+
AttribDefs.GrpcStatusCode.TrySetValue(attribVals, _grpcStatusCode);
141+
}
142+
}

src/Agent/NewRelic/Agent/Core/Segments/NoOpSegment.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using NewRelic.Agent.Api;
66
using NewRelic.Agent.Api.Experimental;
77
using NewRelic.Agent.Core.Attributes;
8+
using NewRelic.Agent.Extensions.Api.Experimental;
89

910
namespace NewRelic.Agent.Core.Segments;
1011

@@ -83,5 +84,15 @@ public string GetCategory()
8384
return string.Empty;
8485
}
8586

87+
public INewRelicActivity GetActivity()
88+
{
89+
return null;
90+
}
91+
92+
public void AddCacheItem(string key, object value)
93+
{
94+
return;
95+
}
96+
8697
public TimeSpan DurationOrZero => TimeSpan.Zero;
87-
}
98+
}

src/Agent/NewRelic/Agent/Core/Segments/Segment.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ public class Segment : IInternalSpan, ISegmentDataState, IHybridAgentSegment
9797
{
9898
private static ConfigurationSubscriber _configurationSubscriber = new ConfigurationSubscriber();
9999

100+
// Used to store _small_ amounts of data for use when creating segment data or other attributes. This is not intended to be a general purpose storage mechanism and should not be used to store large objects or large amounts of data.
101+
private Dictionary<string, object> _dataCache;
102+
100103
public IAttributeDefinitions AttribDefs => _transactionSegmentState.AttribDefs;
101104
public string TypeName => MethodCallData.TypeName;
102105

@@ -124,6 +127,7 @@ public Segment(ITransactionSegmentState transactionSegmentState, MethodCallData
124127
Combinable = false;
125128
IsLeaf = false;
126129
IsAsync = methodCallData.IsAsync;
130+
_dataCache = new Dictionary<string, object>();
127131
}
128132

129133
/// <summary>
@@ -147,6 +151,7 @@ public Segment(ITransactionSegmentState transactionSegmentState, MethodCallData
147151
Combinable = false;
148152
IsLeaf = true;
149153
IsAsync = methodCallData.IsAsync;
154+
_dataCache = new Dictionary<string, object>();
150155
}
151156

152157
/// <summary>
@@ -178,6 +183,7 @@ public Segment(TimeSpan relativeStartTime, TimeSpan? duration, Segment segment,
178183

179184
SpanId = segment.SpanId;
180185
IsAsync = segment.IsAsync;
186+
_dataCache = new Dictionary<string, object>();
181187
}
182188

183189
public bool IsDone
@@ -283,6 +289,11 @@ public void SetActivity(INewRelicActivity activity)
283289
_activity = activity;
284290
}
285291

292+
public INewRelicActivity GetActivity()
293+
{
294+
return _activity;
295+
}
296+
286297
public void MakeCombinable()
287298
{
288299
Combinable = true;
@@ -653,6 +664,31 @@ public string GetCategory()
653664
return EnumNameCache<SpanCategory>.GetName(Data.SpanCategory);
654665
}
655666

667+
// See ISegmentExperimental for details on this method.
668+
public void AddCacheItem(string key, object value)
669+
{
670+
if (string.IsNullOrWhiteSpace(key))
671+
{
672+
return;
673+
}
674+
675+
_dataCache[key] = value;
676+
}
677+
678+
/// <summary>
679+
/// Gets an item from the segment's cache with the specified key. Returns null if the key is not found or if the key is null or whitespace.
680+
/// </summary>
681+
/// <param name="key"></param>
682+
/// <returns></returns>
683+
internal object GetCacheItem(string key)
684+
{
685+
if (string.IsNullOrWhiteSpace(key) || !_dataCache.TryGetValue(key, out var item))
686+
{
687+
return null;
688+
}
689+
690+
return item;
691+
}
656692
}
657693

658694
// TODO: Rename this experimental to something else, or find a better way to solve this problem.

src/Agent/NewRelic/Agent/Core/WireModels/MetricWireModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,10 +361,10 @@ public static void TryBuildMessageBrokerSerializationSegmentMetric(string vendor
361361
txStats.MergeUnscopedStats(proposedName, data);
362362
}
363363

364-
public static void TryBuildExternalSegmentMetric(string host, string method, TimeSpan totalTime,
364+
public static void TryBuildExternalSegmentMetric(string host, string method, string library, TimeSpan totalTime,
365365
TimeSpan totalExclusiveTime, TransactionMetricStatsCollection txStats, bool unscopedOnly)
366366
{
367-
var proposedName = MetricNames.GetExternalHost(host, "Stream", method);
367+
var proposedName = MetricNames.GetExternalHost(host, library, method);
368368
var data = MetricDataWireModel.BuildTimingData(totalTime, totalExclusiveTime);
369369
txStats.MergeUnscopedStats(proposedName, data);
370370
if (!unscopedOnly)

src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Api/Experimental/ISegmentExperimental.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright 2020 New Relic, Inc. All rights reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
using NewRelic.Agent.Extensions.Api.Experimental;
5+
46
namespace NewRelic.Agent.Api.Experimental;
57

68
/// <summary>
@@ -54,4 +56,25 @@ public interface ISegmentExperimental
5456
/// Will be true if a relative end time has been set on the segment. In most situations, this is only set when a segment is ended.
5557
/// </summary>
5658
bool IsDone { get; }
57-
}
59+
60+
/// <summary>
61+
/// Returns the activity associated with the segment.
62+
/// </summary>
63+
/// <returns></returns>
64+
INewRelicActivity GetActivity();
65+
66+
/// <summary>
67+
/// Add object to the segment's data cache with the specified key.
68+
/// This is intended for use in cases where you need to store some data during the execution of a segment that will be used when creating the segment data or attributes.
69+
/// This should not be used as a general purpose storage mechanism and should not be used to store large objects or large amounts of data.
70+
/// This will overwrite any existing value in the cache with the same key, so it should be used with unique keys to avoid collisions.
71+
/// It is the caller's responsibility to manage the keys and ensure they are unique within the context of a segment.
72+
///
73+
/// Notes:
74+
/// Null keys are not allowed and will be ignored. If a null key is passed to this method, the method will return without adding anything to the cache.
75+
/// Null values are allowed and will be stored in the cache. If a null value is added to the cache, it will overwrite any existing value with the same key, and a subsequent retrieval of that key from the cache will return null.
76+
/// </summary>
77+
/// <param name="key"></param>
78+
/// <param name="value"></param>
79+
void AddCacheItem(string key, object value);
80+
}

src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/HttpClient/SendAsync.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins
5959

6060
var transactionExperimental = transaction.GetExperimentalApi();
6161

62+
// Where we need to find the previous segment or activity and if it is a gRPC segment/activity, we need to set the server.address and server.port attributes on the segment/activity.
63+
// After we do this, we need to no-op this so that we only create a single external segment.
64+
// This is required for distributed tracing to work properly for gRPC requests.
65+
// No segment has been created in this wrapper at this point, so this should be the parent.
66+
var currentSegment = (ISegmentExperimental)transaction.CurrentSegment;
67+
if (currentSegment.GetActivity().DisplayName == "Grpc.Net.Client.GrpcOut")
68+
{
69+
// Using uri here since we ensure that it has both host and port information in TryGetAbsoluteUri;
70+
currentSegment.AddCacheItem("server.address", uri.Host);
71+
currentSegment.AddCacheItem("server.port", uri.Port);
72+
return Delegates.NoOp;
73+
}
74+
6275
var externalSegmentData = transactionExperimental.CreateExternalSegmentData(uri, method);
6376
var segment = transactionExperimental.StartSegment(instrumentedMethodCall.MethodCall);
6477
segment.GetExperimentalApi()
@@ -208,4 +221,4 @@ private static KeyValuePair<string, string> Flatten(KeyValuePair<string, IEnumer
208221

209222
return new KeyValuePair<string, string>(key, flattenedValues);
210223
}
211-
}
224+
}

0 commit comments

Comments
 (0)