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

[AzureMonitorExporter] Add support new Messaging semantics - Request/Dependency Telemetry #37508

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,11 @@ private void SetRpcDependencyProperties(ref AzMonList rpcTagObjects)

private void SetMessagingDependencyProperties(Activity activity, ref AzMonList messagingTagObjects)
{
var messagingAttributeTagObjects = AzMonList.GetTagValues(ref messagingTagObjects, SemanticConventions.AttributeMessagingUrl, SemanticConventions.AttributeMessagingSystem);
var messagingUrl = messagingAttributeTagObjects[0]?.ToString();
var messagingSystem = AzMonList.GetTagValue(ref messagingTagObjects, SemanticConventions.AttributeMessagingSystem);
var (messagingUrl, target) = messagingTagObjects.GetMessagingUrlAndSourceOrTarget(activity.Kind);
Data = messagingUrl?.Truncate(SchemaConstants.RemoteDependencyData_Data_MaxLength);
Type = messagingAttributeTagObjects[1]?.ToString().Truncate(SchemaConstants.RemoteDependencyData_Type_MaxLength);
Target = target.Truncate(SchemaConstants.RemoteDependencyData_Target_MaxLength);
Type = messagingSystem?.ToString().Truncate(SchemaConstants.RemoteDependencyData_Type_MaxLength);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@

using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using Azure.Core;
using Azure.Monitor.OpenTelemetry.Exporter.Internals;

namespace Azure.Monitor.OpenTelemetry.Exporter.Models
{
internal partial class RequestData
{
public RequestData(int version, string? operationName, string? requestUrl, Activity activity, ref ActivityTagsProcessor activityTagsProcessor) : this(version, activity, ref activityTagsProcessor)
{
Name = operationName;
Url = requestUrl;
}

public RequestData(int version, Activity activity, ref ActivityTagsProcessor activityTagsProcessor) : base(version)
{
string? responseCode = null;
Expand Down Expand Up @@ -50,6 +57,7 @@ public RequestData(int version, Activity activity, ref ActivityTagsProcessor act
TraceHelper.AddPropertiesToTelemetry(Properties, ref activityTagsProcessor.UnMappedTags);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsSuccess(Activity activity, string? responseCode, OperationType operationType)
{
if (operationType.HasFlag(OperationType.Http)
Expand All @@ -67,25 +75,27 @@ internal static bool IsSuccess(Activity activity, string? responseCode, Operatio

private void SetHttpRequestPropertiesAndResponseCode(Activity activity, ref AzMonList httpTagObjects, out string responseCode)
{
Url = httpTagObjects.GetRequestUrl().Truncate(SchemaConstants.RequestData_Url_MaxLength);
Name = TraceHelper.GetOperationName(activity, ref httpTagObjects).Truncate(SchemaConstants.RequestData_Name_MaxLength);
Url ??= httpTagObjects.GetRequestUrl().Truncate(SchemaConstants.RequestData_Url_MaxLength);
Name ??= TraceHelper.GetOperationName(activity, ref httpTagObjects).Truncate(SchemaConstants.RequestData_Name_MaxLength);
responseCode = AzMonList.GetTagValue(ref httpTagObjects, SemanticConventions.AttributeHttpStatusCode)
?.ToString().Truncate(SchemaConstants.RequestData_ResponseCode_MaxLength)
?? "0";
}

private void SetHttpV2RequestPropertiesAndResponseCode(Activity activity, ref AzMonList httpTagObjects, out string responseCode)
{
Url = httpTagObjects.GetNewSchemaRequestUrl().Truncate(SchemaConstants.RequestData_Url_MaxLength);
Name = TraceHelper.GetNewSchemaOperationName(activity, Url, ref httpTagObjects).Truncate(SchemaConstants.RequestData_Name_MaxLength);
Url ??= httpTagObjects.GetNewSchemaRequestUrl().Truncate(SchemaConstants.RequestData_Url_MaxLength);
Name ??= TraceHelper.GetNewSchemaOperationName(activity, Url, ref httpTagObjects).Truncate(SchemaConstants.RequestData_Name_MaxLength);
responseCode = AzMonList.GetTagValue(ref httpTagObjects, SemanticConventions.AttributeHttpResponseStatusCode)
?.ToString().Truncate(SchemaConstants.RequestData_ResponseCode_MaxLength)
?? "0";
}

private void SetMessagingRequestProperties(Activity activity, ref AzMonList messagingTagObjects)
{
Url = AzMonList.GetTagValue(ref messagingTagObjects, SemanticConventions.AttributeMessagingUrl)?.ToString().Truncate(SchemaConstants.RequestData_Url_MaxLength);
var (messagingUrl, source) = messagingTagObjects.GetMessagingUrlAndSourceOrTarget(activity.Kind);
Url = messagingUrl.Truncate(SchemaConstants.RequestData_Url_MaxLength);
Source = source.Truncate(SchemaConstants.RequestData_Source_MaxLength);
Name = activity.DisplayName;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,6 @@ public TelemetryItem(Activity activity, ref ActivityTagsProcessor activityTagsPr
}

SetAuthenticatedUserId(ref activityTagsProcessor);

// we only have mapping for server spans
// todo: non-server spans
if (activity.Kind == ActivityKind.Server)
{
Tags[ContextTagKeys.AiOperationName.ToString()] = activityTagsProcessor.activityType.HasFlag(OperationType.V2)
? TraceHelper.GetNewSchemaOperationName(activity, null, ref activityTagsProcessor.MappedTags)
: TraceHelper.GetOperationName(activity, ref activityTagsProcessor.MappedTags);
Tags[ContextTagKeys.AiLocationIp.ToString()] = TraceHelper.GetLocationIp(ref activityTagsProcessor.MappedTags);
}

SetResourceSdkVersionAndIkey(resource, instrumentationKey);
if (AzMonList.GetTagValue(ref activityTagsProcessor.MappedTags, "sampleRate") is float sampleRate)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,8 @@ internal struct ActivityTagsProcessor
SemanticConventions.AttributeEndpointAddress,
// required - Messaging
SemanticConventions.AttributeMessagingSystem,
SemanticConventions.AttributeMessagingDestination,
SemanticConventions.AttributeMessagingDestinationKind,
SemanticConventions.AttributeMessagingTempDestination,
SemanticConventions.AttributeMessagingUrl,
SemanticConventions.AttributeMessagingDestinationName,
SemanticConventions.AttributeNetworkProtocolName,

// Others
SemanticConventions.AttributeEnduserId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace Azure.Monitor.OpenTelemetry.Exporter.Internals;
Expand All @@ -15,31 +16,79 @@ internal static class AzMonNewListExtensions
{
try
{
var serverAddress = AzMonList.GetTagValue(ref tagObjects, SemanticConventions.AttributeServerAddress)?.ToString();
if (serverAddress != null)
var requestUrlTagObjects = AzMonList.GetTagValues(ref tagObjects, SemanticConventions.AttributeUrlScheme, SemanticConventions.AttributeServerAddress, SemanticConventions.AttributeServerPort, SemanticConventions.AttributeUrlPath, SemanticConventions.AttributeUrlQuery);

var scheme = requestUrlTagObjects[0]?.ToString() ?? string.Empty; // requestUrlTagObjects[0] => SemanticConventions.AttributeUrlScheme.
var host = requestUrlTagObjects[1]?.ToString() ?? string.Empty; // requestUrlTagObjects[1] => SemanticConventions.AttributeServerAddress.
var port = requestUrlTagObjects[2]?.ToString(); // requestUrlTagObjects[2] => SemanticConventions.AttributeServerPort.
port = port != null ? port = $":{port}" : string.Empty;
var path = requestUrlTagObjects[3]?.ToString() ?? string.Empty; // requestUrlTagObjects[3] => SemanticConventions.AttributeUrlPath.
var queryString = requestUrlTagObjects[4]?.ToString() ?? string.Empty; // requestUrlTagObjects[4] => SemanticConventions.AttributeUrlQuery.

var length = scheme.Length + Uri.SchemeDelimiter.Length + host.Length + port.Length + path.Length + queryString.Length;

var urlStringBuilder = new System.Text.StringBuilder(length)
.Append(scheme)
.Append(Uri.SchemeDelimiter)
.Append(host)
.Append(port)
.Append(path)
.Append(queryString);

return urlStringBuilder.ToString();
}
catch
{
// If URI building fails, there is no need to throw an exception. Instead, we can simply return null.
}

return null;
}

///<summary>
/// Gets messaging url from activity tag objects.
///</summary>
internal static (string? MessagingUrl, string? SourceOrTarget) GetMessagingUrlAndSourceOrTarget(this AzMonList tagObjects, ActivityKind activityKind)
{
string? messagingUrl = null;
string? sourceOrTarget = null;

try
{
var host = AzMonList.GetTagValue(ref tagObjects, SemanticConventions.AttributeServerAddress)?.ToString()
?? AzMonList.GetTagValue(ref tagObjects, SemanticConventions.AttributeNetPeerName)?.ToString();
if (!string.IsNullOrEmpty(host))
{
UriBuilder uriBuilder = new()
{
Scheme = AzMonList.GetTagValue(ref tagObjects, SemanticConventions.AttributeUrlScheme)?.ToString(),
Host = serverAddress,
Path = AzMonList.GetTagValue(ref tagObjects, SemanticConventions.AttributeUrlPath)?.ToString(),
Query = AzMonList.GetTagValue(ref tagObjects, SemanticConventions.AttributeUrlQuery)?.ToString()
};
object?[] messagingTagObjects;

if (int.TryParse(AzMonList.GetTagValue(ref tagObjects, SemanticConventions.AttributeServerPort)?.ToString(), out int port))
messagingTagObjects = AzMonList.GetTagValues(ref tagObjects, SemanticConventions.AttributeNetworkProtocolName, SemanticConventions.AttributeMessagingDestinationName);
var protocolName = messagingTagObjects[0]?.ToString() ?? string.Empty; // messagingTagObjects[0] => SemanticConventions.AttributeNetworkProtocolName.
var destinationName = messagingTagObjects[1]?.ToString() ?? string.Empty; // messagingTagObjects[1] => SemanticConventions.AttributeMessagingDestinationName.

if (destinationName.Length > 0)
{
uriBuilder.Port = port;
destinationName = $"/{destinationName}";
}

return uriBuilder.Uri.AbsoluteUri;
sourceOrTarget = $"{host}{destinationName}";

var length = protocolName.Length + (protocolName?.Length > 0 ? Uri.SchemeDelimiter.Length : 0) + host!.Length + destinationName.Length;

var messagingStringBuilder = new System.Text.StringBuilder(length)
.Append(protocolName)
.Append(string.IsNullOrEmpty(protocolName) ? null : Uri.SchemeDelimiter)
.Append(host)
.Append(destinationName);

messagingUrl = messagingStringBuilder.ToString();
}
}
catch
{
// If URI building fails, there is no need to throw an exception. Instead, we can simply return null.
// If Messaging Url building fails, there is no need to throw an exception. Instead, we can simply return null.
}

return null;
return (MessagingUrl: messagingUrl, SourceOrTarget: sourceOrTarget);
}

///<summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,5 +163,9 @@ internal static class SemanticConventions
public const string AttributeUrlQuery = "url.query";
public const string AttributeUserAgentOriginal = "user_agent.original"; // replaces: "http.user_agent" (AttributeHttpUserAgent)
public const string AttributeServerSocketAddress = "server.socket.address"; // replaces: "net.peer.ip" (AttributeNetPeerIp)

// Messaging v1.21.0 https://github.com/open-telemetry/opentelemetry-specification/blob/v1.21.0/specification/trace/semantic_conventions/messaging.md
public const string AttributeMessagingDestinationName = "messaging.destination.name";
public const string AttributeNetworkProtocolName = "network.protocol.name";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,29 @@ internal static List<TelemetryItem> OtelToAzureMonitorTrace(Batch<Activity> batc
var activityTagsProcessor = EnumerateActivityTags(activity);
telemetryItem = new TelemetryItem(activity, ref activityTagsProcessor, azureMonitorResource, instrumentationKey);

// Check for Exceptions events
if (activity.Events.Any())
{
AddTelemetryFromActivityEvents(activity, telemetryItem, telemetryItems);
}

switch (activity.GetTelemetryType())
{
case TelemetryType.Request:
telemetryItem.Data = new MonitorBase
if (activity.Kind == ActivityKind.Server)
{
BaseType = "RequestData",
BaseData = new RequestData(Version, activity, ref activityTagsProcessor)
};
var (requestUrl, operationName) = GetHttpOperationNameAndUrl(activity.DisplayName, activityTagsProcessor.activityType, ref activityTagsProcessor.MappedTags);
telemetryItem.Tags[ContextTagKeys.AiOperationName.ToString()] = operationName;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TimothyMothra We may need a follow up PR here to check if this requires Truncation. Truncation was not a part of existing implementation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found the length for AIOperationName. It's in an internal repo. I'll get this updated

telemetryItem.Tags[ContextTagKeys.AiLocationIp.ToString()] = TraceHelper.GetLocationIp(ref activityTagsProcessor.MappedTags);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vishweshbankwar I think for new HTTP semantics, we need to add client.address. We could follow up on a new PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also UserAgent

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TimothyMothra Is there a plan to add client.address to AspNetCoreInstrumentation library?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no "plan" at the moment.
When working on the instrumentation library, I only converted the attributes that were already in use. I didn't add any new attributes.

The spec describes client.address as Recommended, but not Required.
Lets discuss this in standup.


telemetryItem.Data = new MonitorBase
{
BaseType = "RequestData",
BaseData = new RequestData(Version, operationName, requestUrl, activity, ref activityTagsProcessor)
};
}
else
{
telemetryItem.Data = new MonitorBase
{
BaseType = "RequestData",
BaseData = new RequestData(Version, activity, ref activityTagsProcessor)
};
}
break;
case TelemetryType.Dependency:
telemetryItem.Data = new MonitorBase
Expand All @@ -60,6 +69,12 @@ internal static List<TelemetryItem> OtelToAzureMonitorTrace(Batch<Activity> batc
break;
}

// Check for Exceptions events
if (activity.Events.Any())
{
AddTelemetryFromActivityEvents(activity, telemetryItem, telemetryItems);
}

activityTagsProcessor.Return();
telemetryItems.Add(telemetryItem);
}
Expand Down Expand Up @@ -171,7 +186,7 @@ internal static string GetOperationName(Activity activity, ref AzMonList MappedT
return activity.DisplayName;
}

internal static string GetNewSchemaOperationName(Activity activity, string? url, ref AzMonList MappedTags)
internal static string GetNewSchemaOperationName(Activity activity, string? url, ref AzMonList MappedTags)
{
var httpMethod = AzMonList.GetTagValue(ref MappedTags, SemanticConventions.AttributeHttpRequestMethod)?.ToString();
if (!string.IsNullOrWhiteSpace(httpMethod))
Expand All @@ -195,6 +210,42 @@ internal static string GetNewSchemaOperationName(Activity activity, string? url,
return activity.DisplayName;
}

internal static (string? RequestUrl, string? OperationName) GetHttpOperationNameAndUrl(string activityDisplayName, OperationType operationType, ref AzMonList httpMappedTags)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TelemetryItem and RequestData classes were modified to address an issue where the URL formation was being parsed twice. To resolve this new method was introduced that reads the URL and operation name once for each span / telemetry. This approach eliminates redundant parsing and improves the efficiency of the code for ActivityKind.Server for http spans.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this logic also needs to change. We no longer need to rely on url in new schema as we will have the path available. Simple http.method + path concat will be faster/cheaper than Uri.TryCreate().

Can be done as a follow up PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, thought about it and felt better to be followed in a new PR.

{
string? httpMethod;
string? httpUrl;

if (operationType.HasFlag(OperationType.V2))
{
httpUrl = httpMappedTags.GetNewSchemaRequestUrl();
httpMethod = AzMonList.GetTagValue(ref httpMappedTags, SemanticConventions.AttributeHttpRequestMethod)?.ToString();
}
else
{
httpUrl = AzMonList.GetTagValue(ref httpMappedTags, SemanticConventions.AttributeHttpUrl)?.ToString();
httpMethod = AzMonList.GetTagValue(ref httpMappedTags, SemanticConventions.AttributeHttpMethod)?.ToString();
}

if (!string.IsNullOrWhiteSpace(httpMethod))
{
var httpRoute = AzMonList.GetTagValue(ref httpMappedTags, SemanticConventions.AttributeHttpRoute)?.ToString();

// ASP.NET instrumentation assigns route as {controller}/{action}/{id} which would result in the same name for different operations.
// To work around that we will use path from httpUrl.
if (httpRoute?.Contains("{controller}") == false)
{
return (RequestUrl: httpUrl, OperationName: $"{httpMethod} {httpRoute}");
}

if (!string.IsNullOrWhiteSpace(httpUrl) && Uri.TryCreate(httpUrl!.ToString(), UriKind.RelativeOrAbsolute, out var uri) && uri.IsAbsoluteUri)
{
return (RequestUrl: httpUrl, OperationName: $"{httpMethod} {uri.AbsolutePath}");
}
}

return (RequestUrl: httpUrl, OperationName: activityDisplayName);
}

private static void AddTelemetryFromActivityEvents(Activity activity, TelemetryItem telemetryItem, List<TelemetryItem> telemetryItems)
{
foreach (ref readonly var @event in activity.EnumerateEvents())
Expand Down
Loading