internal sealed class ElasticCompatibilityProcessor()

in src/Elastic.OpenTelemetry.Core/Processors/ElasticCompatibilityProcessor.cs [25:140]


internal sealed class ElasticCompatibilityProcessor(ILogger? logger) : BaseProcessor<Activity>
{
	private readonly ILogger _logger = logger ?? NullLogger.Instance;

	/// <inheritdoc />
	public override void OnEnd(Activity activity)
	{
		if (activity.Kind == ActivityKind.Server)
		{
			// For inbound HTTP requests (server), ASP.NET Core sets the newer semantic conventions in
			// the latest versions. For now, we need to ensure the older semantic conventions are also
			// included on the spans sent to the Elastic backend as the intake system is currently
			// unaware of the newer semantic conventions. We send the older attributes to ensure that
			// the UI functions as expected. The http and net host conventions are required to build
			// up the URL displayed in the trace sample UI within Kibana. This will be fixed in future
			// version of apm-data.

			string? httpScheme = null;
			string? httpTarget = null;
			string? urlScheme = null;
			string? urlPath = null;
			string? urlQuery = null;
			string? netHostName = null;
			int? netHostPort = null;
			string? serverAddress = null;
			int? serverPort = null;

			// We loop once, collecting all the attributes we need for the older and newer
			// semantic conventions. This is a bit more verbose but ensures we don't iterate
			// the tags multiple times.
			foreach (var tag in activity.TagObjects)
			{
				if (tag.Key == HttpScheme)
					httpScheme = ProcessStringAttribute(tag);

				if (tag.Key == HttpTarget)
					httpTarget = ProcessStringAttribute(tag);

				if (tag.Key == UrlScheme)
					urlScheme = ProcessStringAttribute(tag);

				if (tag.Key == UrlPath)
					urlPath = ProcessStringAttribute(tag);

				if (tag.Key == UrlQuery)
					urlQuery = ProcessStringAttribute(tag);

				if (tag.Key == NetHostName)
					netHostName = ProcessStringAttribute(tag);

				if (tag.Key == ServerAddress)
					serverAddress = ProcessStringAttribute(tag);

				if (tag.Key == NetHostPort)
					netHostPort = ProcessIntAttribute(tag);

				if (tag.Key == ServerPort)
					serverPort = ProcessIntAttribute(tag);
			}

			// Set the older semantic convention attributes
			if (httpScheme is null && urlScheme is not null)
				SetStringAttribute(HttpScheme, urlScheme);

			if (httpTarget is null && urlPath is not null)
			{
				var target = urlPath;

				if (urlQuery is not null)
					target += urlQuery;

				SetStringAttribute(HttpTarget, target);
			}

			if (netHostName is null && serverAddress is not null)
				SetStringAttribute(NetHostName, serverAddress);

			if (netHostPort is null && serverPort is not null)
				SetIntAttribute(NetHostPort, serverPort.Value);
		}

		string? ProcessStringAttribute(KeyValuePair<string, object?> tag)
		{
			if (tag.Value is string value)
			{
				_logger.LogFoundTag(nameof(ElasticCompatibilityProcessor), tag.Key, value);
				return value;
			}

			return null;
		}

		int? ProcessIntAttribute(KeyValuePair<string, object?> tag)
		{
			if (tag.Value is int value)
			{
				_logger.LogFoundTag(nameof(ElasticCompatibilityProcessor), tag.Key, value);
				return value;
			}

			return null;
		}

		void SetStringAttribute(string attributeName, string value)
		{
			_logger.LogSetTag(nameof(ElasticCompatibilityProcessor), attributeName, value);
			activity.SetTag(attributeName, value);
		}

		void SetIntAttribute(string attributeName, int value)
		{
			_logger.LogSetTag(nameof(ElasticCompatibilityProcessor), attributeName, value);
			activity.SetTag(attributeName, value);
		}
	}
}