File: Telemetry\AggregatingTelemetryLog.cs
Web Access
Project: src\src\Razor\src\Razor\src\Microsoft.VisualStudio.LanguageServices.Razor\Microsoft.VisualStudio.LanguageServices.Razor.csproj (Microsoft.VisualStudio.LanguageServices.Razor)
// 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.Immutable;
using Microsoft.CodeAnalysis.Razor.Telemetry;
using Microsoft.VisualStudio.Telemetry;
using Microsoft.VisualStudio.Telemetry.Metrics;
using Microsoft.VisualStudio.Telemetry.Metrics.Events;
 
namespace Microsoft.VisualStudio.Razor.Telemetry;
 
/// <summary>
/// Provides a wrapper around the VSTelemetry histogram APIs to support aggregated telemetry. Each instance
/// of this class corresponds to a specific FunctionId operation and can support aggregated values for each
/// metric name logged.
/// </summary>
internal sealed class AggregatingTelemetryLog
{
    // Indicates version information which vs telemetry will use for our aggregated telemetry. This can be used
    // by Kusto queries to filter against telemetry versions which have the specified version and thus desired shape.
    private const string MeterVersion = "0.40";
 
    private readonly IMeter _meter;
    private readonly TelemetryReporter _telemetryReporter;
    private readonly HistogramConfiguration? _histogramConfiguration;
    private readonly string _eventName;
    private readonly object _flushLock;
 
    private ImmutableDictionary<string, (IHistogram<long> Histogram, TelemetryEvent TelemetryEvent, object Lock)> _histograms = ImmutableDictionary<string, (IHistogram<long>, TelemetryEvent, object)>.Empty;
 
    /// <summary>
    /// Creates a new aggregating telemetry log
    /// </summary>
    /// <param name="reporter">Telemetry reporter for posting events</param>
    /// <param name="name">the name of the event, not fully qualified.</param>
    /// <param name="bucketBoundaries">Optional values indicating bucket boundaries in milliseconds. If not specified, 
    /// all histograms created will use the default histogram configuration</param>
    public AggregatingTelemetryLog(TelemetryReporter reporter, string name, double[]? bucketBoundaries)
    {
        var meterProvider = new VSTelemetryMeterProvider();
 
        _telemetryReporter = reporter;
        _meter = meterProvider.CreateMeter(TelemetryReporter.GetPropertyName("meter"), version: MeterVersion);
        _eventName = TelemetryReporter.GetEventName(name);
        _flushLock = new();
 
        if (bucketBoundaries != null)
        {
            _histogramConfiguration = new HistogramConfiguration(bucketBoundaries);
        }
    }
 
    /// <summary>
    /// Adds aggregated information for the <paramref name="histogramKey"/> and <paramref name="value"/>. Method name is tacked onto
    /// to the first <paramref name="histogramKey"/> used for convenience.
    /// </summary>
    public void Log(
        string histogramKey,
        int value,
        string method)
    {
        if (!IsEnabled)
            return;
 
        (var histogram, _, var histogramLock) = ImmutableInterlocked.GetOrAdd(ref _histograms, histogramKey, histogramKey =>
        {
            var telemetryEvent = new TelemetryEvent(_eventName);
 
            TelemetryReporter.AddToProperties(telemetryEvent.Properties, new Property("method", method));
 
            var histogram = _meter.CreateHistogram<long>(histogramKey, _histogramConfiguration);
            var histogramLock = new object();
 
            return (histogram, telemetryEvent, histogramLock);
        });
 
        lock (histogramLock)
        {
            histogram.Record(value);
        }
    }
 
    private bool IsEnabled => _telemetryReporter.IsEnabled;
 
    public void Flush()
    {
        // This lock ensures that multiple calls to Flush cannot occur simultaneously.
        //  Without this lock, we would could potentially call PostMetricEvent multiple
        //  times for the same histogram.
        lock (_flushLock)
        {
            foreach (var (histogram, telemetryEvent, histogramLock) in _histograms.Values)
            {
                // This fine-grained lock ensures that the histogram isn't modified (via a Record call)
                //  during the creation of the TelemetryHistogramEvent or the PostMetricEvent
                //  call that operates on it.
                lock (histogramLock)
                {
                    var histogramEvent = new TelemetryHistogramEvent<long>(telemetryEvent, histogram);
 
                    _telemetryReporter.ReportMetric(histogramEvent);
                }
            }
 
            _histograms = ImmutableDictionary<string, (IHistogram<long>, TelemetryEvent, object)>.Empty;
        }
    }
 
    public class TelemetryInstrumentEvent(TelemetryEvent telemetryEvent, IInstrument instrument) : TelemetryMetricEvent(telemetryEvent, instrument)
    {
        public TelemetryEvent Event { get; } = telemetryEvent;
        public IInstrument Instrument { get; } = instrument;
    }
 
    public class TelemetryHistogramEvent<T>(TelemetryEvent telemetryEvent, IHistogram<T> histogram) : TelemetryInstrumentEvent(telemetryEvent, histogram)
        where T : struct
    {
    }
}