File: System\Net\NameResolutionTelemetry.cs
Web Access
Project: src\src\libraries\System.Net.NameResolution\src\System.Net.NameResolution.csproj (System.Net.NameResolution)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.Net.Sockets;
using System.Threading;
 
namespace System.Net
{
    [EventSource(Name = "System.Net.NameResolution")]
    internal sealed class NameResolutionTelemetry : EventSource
    {
        public static readonly NameResolutionTelemetry Log = new NameResolutionTelemetry();
 
        private const int ResolutionStartEventId = 1;
        private const int ResolutionStopEventId = 2;
        private const int ResolutionFailedEventId = 3;
 
        private PollingCounter? _lookupsRequestedCounter;
        private PollingCounter? _currentLookupsCounter;
        private EventCounter? _lookupsDuration;
 
        private long _lookupsRequested;
        private long _currentLookups;
 
        protected override void OnEventCommand(EventCommandEventArgs command)
        {
            if (command.Command == EventCommand.Enable)
            {
                // The cumulative number of name resolution requests started since events were enabled
                _lookupsRequestedCounter ??= new PollingCounter("dns-lookups-requested", this, () => Interlocked.Read(ref _lookupsRequested))
                {
                    DisplayName = "DNS Lookups Requested"
                };
 
                // Current number of DNS requests pending
                _currentLookupsCounter ??= new PollingCounter("current-dns-lookups", this, () => Interlocked.Read(ref _currentLookups))
                {
                    DisplayName = "Current DNS Lookups"
                };
 
                _lookupsDuration ??= new EventCounter("dns-lookups-duration", this)
                {
                    DisplayName = "Average DNS Lookup Duration",
                    DisplayUnits = "ms"
                };
            }
        }
 
        [Event(ResolutionStartEventId, Level = EventLevel.Informational)]
        private void ResolutionStart(string hostNameOrAddress) => WriteEvent(ResolutionStartEventId, hostNameOrAddress);
 
        [Event(ResolutionStopEventId, Level = EventLevel.Informational)]
        private void ResolutionStop() => WriteEvent(ResolutionStopEventId);
 
        [Event(ResolutionFailedEventId, Level = EventLevel.Informational)]
        private void ResolutionFailed() => WriteEvent(ResolutionFailedEventId);
 
        [NonEvent]
        public static bool AnyDiagnosticsEnabled() => !OperatingSystem.IsWasi() && (Log.IsEnabled() || NameResolutionMetrics.IsEnabled() || NameResolutionActivity.IsTracingEnabled());
 
        [NonEvent]
        public NameResolutionActivity BeforeResolution(object hostNameOrAddress, long startingTimestamp = 0)
        {
            if (!AnyDiagnosticsEnabled())
            {
                return default;
            }
 
            if (IsEnabled())
            {
                Interlocked.Increment(ref _lookupsRequested);
                Interlocked.Increment(ref _currentLookups);
 
                if (IsEnabled(EventLevel.Informational, EventKeywords.None))
                {
                    string host = GetHostnameFromStateObject(hostNameOrAddress);
 
                    ResolutionStart(host);
                }
 
                startingTimestamp = startingTimestamp is not 0 ? startingTimestamp : Stopwatch.GetTimestamp();
            }
 
            startingTimestamp = startingTimestamp is not 0 ? startingTimestamp : NameResolutionMetrics.IsEnabled() ? Stopwatch.GetTimestamp() : 0;
            return new NameResolutionActivity(hostNameOrAddress, startingTimestamp);
        }
 
        [NonEvent]
        public void AfterResolution(object hostNameOrAddress, in NameResolutionActivity activity, object? answer, Exception? exception = null)
        {
            if (OperatingSystem.IsWasi()) return;
 
            if (!activity.Stop(answer, exception, out TimeSpan duration))
            {
                // We stopped the System.Diagnostics.Activity at this point and neither metrics nor EventSource is enabled.
                return;
            }
 
            if (IsEnabled())
            {
                Interlocked.Decrement(ref _currentLookups);
 
                _lookupsDuration?.WriteMetric(duration.TotalMilliseconds);
 
                if (IsEnabled(EventLevel.Informational, EventKeywords.None))
                {
                    if (exception is not null)
                    {
                        ResolutionFailed();
                    }
 
                    ResolutionStop();
                }
            }
 
            if (NameResolutionMetrics.IsEnabled())
            {
                NameResolutionMetrics.AfterResolution(duration, GetHostnameFromStateObject(hostNameOrAddress), exception);
            }
        }
 
        [NonEvent]
        internal static string GetHostnameFromStateObject(object hostNameOrAddress)
        {
            Debug.Assert(hostNameOrAddress is not null);
 
            string host = hostNameOrAddress switch
            {
                string h => h,
                KeyValuePair<string, AddressFamily> t => t.Key,
                IPAddress a => a.ToString(),
                KeyValuePair<IPAddress, AddressFamily> t => t.Key.ToString(),
                _ => null!
            };
 
            Debug.Assert(host is not null, $"Unknown hostNameOrAddress type: {hostNameOrAddress.GetType().Name}");
 
            return host;
        }
 
        [NonEvent]
        internal static string GetErrorType(Exception exception) => (exception as SocketException)?.SocketErrorCode switch
        {
            SocketError.HostNotFound => "host_not_found",
            SocketError.TryAgain => "try_again",
            SocketError.AddressFamilyNotSupported => "address_family_not_supported",
            SocketError.NoRecovery => "no_recovery",
 
            _ => exception.GetType().FullName!
        };
    }
 
    /// <summary>
    /// Encapsulates the starting timestamp together with an optional Activity, to represent the name resolution span for various telemetry pillars.
    /// </summary>
    internal readonly struct NameResolutionActivity
    {
        private const string ActivitySourceName = "Experimental.System.Net.NameResolution";
        private const string ActivityName = ActivitySourceName + ".DnsLookup";
        private static readonly ActivitySource s_activitySource = new ActivitySource(ActivitySourceName);
 
        // _startingTimestamp == 0 means NameResolutionTelemetry and NameResolutionMetrics are both disabled.
        private readonly long _startingTimestamp;
        private readonly Activity? _activity;
 
        public NameResolutionActivity(object hostNameOrAddress, long startingTimestamp)
        {
            _startingTimestamp = startingTimestamp;
            _activity = s_activitySource.StartActivity(ActivityName);
            if (_activity is not null)
            {
                string host = NameResolutionTelemetry.GetHostnameFromStateObject(hostNameOrAddress);
                _activity.DisplayName = hostNameOrAddress is IPAddress ? $"DNS reverse lookup {host}" : $"DNS lookup {host}";
                if (_activity.IsAllDataRequested)
                {
                    _activity.SetTag("dns.question.name", host);
                }
            }
        }
 
        public static bool IsTracingEnabled() => s_activitySource.HasListeners();
 
        // Returns true if either NameResolutionTelemetry or NameResolutionMetrics is enabled.
        public bool Stop(object? answer, Exception? exception, out TimeSpan duration)
        {
            if (_activity is not null)
            {
                if (_activity.IsAllDataRequested)
                {
                    if (answer is not null)
                    {
                        string[]? answerValues = answer switch
                        {
                            string h => [h],
                            IPAddress[] addresses => GetStringValues(addresses),
                            IPHostEntry entry => GetStringValues(entry.AddressList),
                            _ => null
                        };
 
                        Debug.Assert(answerValues is not null);
                        _activity.SetTag("dns.answers", answerValues);
                    }
                    else
                    {
                        Debug.Assert(exception is not null);
                        string errorType = NameResolutionTelemetry.GetErrorType(exception);
                        _activity.SetTag("error.type", errorType);
                    }
                }
 
                if (exception is not null)
                {
                    _activity.SetStatus(ActivityStatusCode.Error);
                }
 
                _activity.Stop();
            }
 
            if (_startingTimestamp == 0)
            {
                duration = default;
                return false;
            }
 
            duration = Stopwatch.GetElapsedTime(_startingTimestamp);
            return true;
 
            static string[] GetStringValues(IPAddress[] addresses)
            {
                string[] result = new string[addresses.Length];
                for (int i = 0; i < addresses.Length; i++)
                {
                    result[i] = addresses[i].ToString();
                }
                return result;
            }
        }
    }
}