File: Telemetry\TelemetryActivity.cs
Web Access
Project: src\nuget-client\src\NuGet.Core\NuGet.Common\NuGet.Common.csproj (NuGet.Common)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;

namespace NuGet.Common
{
    /// <summary> Represents telemetry activity which spans a time interval. </summary>
    /// <remarks> Always dispose the activity at the end of interval covered. </remarks>
    public class TelemetryActivity : IDisposable
    {
        private readonly DateTime _startTime;
        private readonly Stopwatch _stopwatch;
        private readonly Stopwatch _intervalWatch = new Stopwatch();
        private readonly List<Tuple<string, TimeSpan>> _intervalList;
        private readonly IDisposable? _telemetryActivity;
        private bool _disposed;

        /// <summary> Telemetry event which represents end of telemetry activity. </summary>
        public TelemetryEvent TelemetryEvent { get; set; }

        /// <summary> Parent activity ID. </summary>
        public Guid ParentId { get; }

        /// <summary> Operation ID. </summary>
        public Guid OperationId { get; }

        /// <summary> Singleton of NuGet telemetry service instance. </summary>
        public static INuGetTelemetryService? NuGetTelemetryService { get; set; }

        private TelemetryActivity(Guid parentId, TelemetryEvent telemetryEvent, Guid operationId)
        {
            if (telemetryEvent != null)
            {
                if (telemetryEvent.Name is null)
                {
                    throw new ArgumentException(paramName: nameof(telemetryEvent), message: "Property 'Name' must not be null");
                }
                _telemetryActivity = NuGetTelemetryService?.StartActivity(telemetryEvent.Name);
            }
            else
            {
                Debug.Fail("Looking at all references to the static Create methods, I don't think this code path is possible.");
            }

            TelemetryEvent = telemetryEvent!;
            ParentId = parentId;
            OperationId = operationId;

            _startTime = DateTime.UtcNow;
            _stopwatch = Stopwatch.StartNew();
            _intervalList = new List<Tuple<string, TimeSpan>>();
        }

        /// <summary> Start interval measure.
        /// End with <see cref="EndIntervalMeasure(string)"/>
        /// The intervals cannot overlap. For non-overlapping intervals <see cref="StartIndependentInterval(string)"/>
        /// </summary>
        public void StartIntervalMeasure()
        {
            _intervalWatch.Restart();
        }

        /// <summary> End interval measure. </summary>
        /// <param name="propertyName"> Property name to represents the interval. </param>
        public void EndIntervalMeasure(string propertyName)
        {
            _intervalWatch.Stop();
            _intervalList.Add(new Tuple<string, TimeSpan>(propertyName, _intervalWatch.Elapsed));
        }

        public IDisposable StartIndependentInterval(string propertyName)
        {
            return new Interval(this, propertyName);
        }

        private class Interval : IDisposable
        {
            private readonly TelemetryActivity _telemetryActivity;
            private readonly string _propertyName;
            private readonly Stopwatch _stopwatch;

            internal Interval(TelemetryActivity telemetryActivity, string propertyName)
            {
                _telemetryActivity = telemetryActivity;
                _propertyName = propertyName;
                _stopwatch = Stopwatch.StartNew();
            }

            void IDisposable.Dispose()
            {
                _stopwatch.Stop();
                _telemetryActivity.TelemetryEvent[_propertyName] = _stopwatch.Elapsed.TotalSeconds;
            }
        }

        /// <summary> Stops tracking the activity and emits a telemetry event. </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }

            if (disposing)
            {
                _stopwatch.Stop();

                if (NuGetTelemetryService != null && TelemetryEvent != null)
                {
                    var endTime = DateTime.UtcNow;
                    TelemetryEvent["StartTime"] = _startTime.ToString("O", CultureInfo.CurrentCulture);
                    TelemetryEvent["EndTime"] = endTime.ToString("O", CultureInfo.CurrentCulture);
                    TelemetryEvent["Duration"] = _stopwatch.Elapsed.TotalSeconds;

                    if (ParentId != Guid.Empty)
                    {
                        TelemetryEvent[nameof(ParentId)] = ParentId.ToString();
                    }

                    if (OperationId != Guid.Empty)
                    {
                        TelemetryEvent[nameof(OperationId)] = OperationId.ToString();
                    }

                    foreach (var interval in _intervalList)
                    {
                        TelemetryEvent[interval.Item1] = interval.Item2.TotalSeconds;
                    }

                    NuGetTelemetryService.EmitTelemetryEvent(TelemetryEvent);
                }

                _telemetryActivity?.Dispose();
            }

            _disposed = true;
        }

        /// <summary> Emit a singular telemetry event. </summary>
        /// <param name="TelemetryEvent"> Telemetry event. </param>
        public static void EmitTelemetryEvent(TelemetryEvent TelemetryEvent)
        {
            NuGetTelemetryService?.EmitTelemetryEvent(TelemetryEvent);
        }

        /// <summary> Creates a TelemetryActivity. </summary>
        /// <param name="eventName"> Name of the event. </param>
        /// <returns> TelemetryActivity with an empty parentId, new operationId, and a TelemetryEvent with eventName. </returns>
        public static TelemetryActivity Create(string eventName)
        {
            return Create(Guid.Empty, new TelemetryEvent(eventName));
        }

        /// <summary> Creates a TelemetryActivity. </summary>
        /// <param name="telemetryEvent"> Telemetry event. </param>
        /// <returns> TelemetryActivity with an empty parentId, new operationId, and given TelemetryEvent. </returns>
        public static TelemetryActivity Create(TelemetryEvent telemetryEvent)
        {
            return Create(Guid.Empty, telemetryEvent);
        }

        /// <summary> Creates a TelemetryActivity. </summary>
        /// <param name="parentId"> OperationId of the parent event. </param>
        /// <param name="eventName"> Name of the event. </param>
        /// <returns> TelemetryActivity with a given parentId, new operationId, and a TelemetryEvent with eventName. </returns>
        public static TelemetryActivity Create(Guid parentId, string eventName)
        {
            return Create(parentId, new TelemetryEvent(eventName));
        }

        /// <summary> Creates a TelemetryActivity. </summary>
        /// <param name="parentId"> OperationId of the parent event. </param>
        /// <param name="telemetryEvent"> Telemetry event. </param>
        /// <returns> TelemetryActivity with a given parentId, new operationId, and given TelemetryEvent. </returns>
        public static TelemetryActivity Create(Guid parentId, TelemetryEvent telemetryEvent)
        {
            return new TelemetryActivity(parentId, telemetryEvent, Guid.NewGuid());
        }
    }
}