|
| 1 | +// Copyright (c) .NET Foundation. All rights reserved. |
| 2 | +// Licensed under the MIT License. See License.txt in the project root for license information. |
| 3 | + |
| 4 | +#nullable enable |
| 5 | + |
| 6 | +using System; |
| 7 | +using System.Collections.Generic; |
| 8 | +using System.Diagnostics.Metrics; |
| 9 | +using System.Linq; |
| 10 | +using System.Threading; |
| 11 | +using System.Threading.Tasks; |
| 12 | +using AwesomeAssertions; |
| 13 | +using Google.Protobuf.WellKnownTypes; |
| 14 | +using Microsoft.ApplicationInsights.Channel; |
| 15 | +using Microsoft.ApplicationInsights.DataContracts; |
| 16 | +using Microsoft.ApplicationInsights.Extensibility; |
| 17 | +using Microsoft.Azure.WebJobs.Script.Diagnostics; |
| 18 | +using Microsoft.Extensions.Options; |
| 19 | +using Moq; |
| 20 | +using Xunit; |
| 21 | + |
| 22 | +namespace Microsoft.Azure.WebJobs.Script.Tests.Diagnostics |
| 23 | +{ |
| 24 | + public sealed class ApplicationInsightsMetricExporterTests : IDisposable |
| 25 | + { |
| 26 | + private const string InstrumentName = "test.instrument"; |
| 27 | + private readonly TelemetryConfiguration _config; |
| 28 | + private readonly List<ITelemetry> _items = []; |
| 29 | + |
| 30 | + public ApplicationInsightsMetricExporterTests() |
| 31 | + { |
| 32 | + Mock<ITelemetryChannel> mockChannel = new(); |
| 33 | + mockChannel.Setup(c => c.Send(It.IsAny<ITelemetry>())) |
| 34 | + .Callback<ITelemetry>(_items.Add); |
| 35 | + |
| 36 | + _config = new TelemetryConfiguration |
| 37 | + { |
| 38 | + ConnectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000", |
| 39 | + TelemetryChannel = mockChannel.Object |
| 40 | + }; |
| 41 | + } |
| 42 | + |
| 43 | + public static Dictionary<string, Action<Meter>> InstrumentActions { get; } = new() |
| 44 | + { |
| 45 | + ["Counter{byte}"] = (meter) => meter.CreateCounter<byte>(InstrumentName).Add(1), |
| 46 | + ["Counter{short}"] = (meter) => meter.CreateCounter<short>(InstrumentName).Add(1), |
| 47 | + ["Counter{int}"] = (meter) => meter.CreateCounter<int>(InstrumentName).Add(1), |
| 48 | + ["Counter{long}"] = (meter) => meter.CreateCounter<long>(InstrumentName).Add(1), |
| 49 | + ["Counter{float}"] = (meter) => meter.CreateCounter<float>(InstrumentName).Add(1), |
| 50 | + ["Counter{double}"] = (meter) => meter.CreateCounter<double>(InstrumentName).Add(1), |
| 51 | + ["Counter{decimal}"] = (meter) => meter.CreateCounter<decimal>(InstrumentName).Add(1), |
| 52 | + ["Histogram{byte}"] = (meter) => meter.CreateHistogram<byte>(InstrumentName).Record(1), |
| 53 | + ["Histogram{short}"] = (meter) => meter.CreateHistogram<short>(InstrumentName).Record(1), |
| 54 | + ["Histogram{int}"] = (meter) => meter.CreateHistogram<int>(InstrumentName).Record(1), |
| 55 | + ["Histogram{long}"] = (meter) => meter.CreateHistogram<long>(InstrumentName).Record(1), |
| 56 | + ["Histogram{float}"] = (meter) => meter.CreateHistogram<float>(InstrumentName).Record(1), |
| 57 | + ["Histogram{double}"] = (meter) => meter.CreateHistogram<double>(InstrumentName).Record(1), |
| 58 | + ["Histogram{decimal}"] = (meter) => meter.CreateHistogram<decimal>(InstrumentName).Record(1), |
| 59 | + }; |
| 60 | + |
| 61 | + public static IEnumerable<object[]> InstrumentTests => InstrumentActions.Keys.Select(k => new object[] { k }); |
| 62 | + |
| 63 | + public void Dispose() |
| 64 | + { |
| 65 | + _config.Dispose(); |
| 66 | + } |
| 67 | + |
| 68 | + [Fact] |
| 69 | + public void Constructor_ThrowsOnNullOptions() |
| 70 | + { |
| 71 | + // act |
| 72 | + Action act = () => new ApplicationInsightsMetricExporter(null!); |
| 73 | + |
| 74 | + // assert |
| 75 | + act.Should().Throw<ArgumentNullException>().WithParameterName("options"); |
| 76 | + } |
| 77 | + |
| 78 | + [Fact] |
| 79 | + public void Initialize_ThrowsOnNullConfiguration() |
| 80 | + { |
| 81 | + // arrange |
| 82 | + ApplicationInsightsMetricExporter exporter = CreateExporter(); |
| 83 | + |
| 84 | + // act |
| 85 | + Action act = () => exporter.Initialize(null!); |
| 86 | + |
| 87 | + // assert |
| 88 | + act.Should().Throw<ArgumentNullException>().WithParameterName("configuration"); |
| 89 | + } |
| 90 | + |
| 91 | + [Fact] |
| 92 | + public void MeterListener_IgnoresInstrumentsNotInConfiguration() |
| 93 | + { |
| 94 | + // arrange |
| 95 | + ApplicationInsightsMetricExporter exporter = CreateExporter("configured.meter"); |
| 96 | + exporter.Initialize(_config); |
| 97 | + |
| 98 | + // act - create instrument from unconfigured meter |
| 99 | + using Meter meter = new("unconfigured.meter"); |
| 100 | + Counter<long> counter = meter.CreateCounter<long>("test.counter"); |
| 101 | + counter.Add(1); |
| 102 | + exporter.Flush(); |
| 103 | + |
| 104 | + // assert - no telemetry should be sent for unconfigured meters |
| 105 | + _items.Should().BeEmpty(); |
| 106 | + } |
| 107 | + |
| 108 | + [Theory] |
| 109 | + [MemberData(nameof(InstrumentTests))] |
| 110 | + public void MeterListener_TracksConfiguredInstruments(string test) |
| 111 | + { |
| 112 | + // arrange |
| 113 | + ApplicationInsightsMetricExporter exporter = CreateExporter("configured.meter"); |
| 114 | + exporter.Initialize(_config); |
| 115 | + |
| 116 | + // Small delay to ensure initialization completes |
| 117 | + Thread.Sleep(100); |
| 118 | + |
| 119 | + // act - create and use instrument from configured meter |
| 120 | + using Meter meter = new("configured.meter"); |
| 121 | + InstrumentActions[test](meter); |
| 122 | + exporter.Flush(); |
| 123 | + |
| 124 | + // Small delay to allow async processing |
| 125 | + Thread.Sleep(100); |
| 126 | + |
| 127 | + _items.Should().ContainSingle() |
| 128 | + .Which.Should().Satisfy<MetricTelemetry>(t => |
| 129 | + { |
| 130 | + t.Name.Should().Be("test.instrument"); |
| 131 | + t.MetricNamespace.Should().Be("configured.meter"); |
| 132 | + t.Sum.Should().Be(Convert.ToDouble(1)); |
| 133 | + }); |
| 134 | + } |
| 135 | + |
| 136 | + private static ApplicationInsightsMetricExporter CreateExporter( |
| 137 | + params ReadOnlySpan<string> meters) |
| 138 | + => new(CreateOptions(meters)); |
| 139 | + |
| 140 | + private static OptionsWrapper<ApplicationInsightsMetricExporterOptions> CreateOptions( |
| 141 | + params ReadOnlySpan<string> meters) |
| 142 | + { |
| 143 | + ApplicationInsightsMetricExporterOptions options = new(); |
| 144 | + foreach (string meter in meters) |
| 145 | + { |
| 146 | + options.Meters.Add(meter); |
| 147 | + } |
| 148 | + |
| 149 | + return new(options); |
| 150 | + } |
| 151 | + } |
| 152 | +} |
0 commit comments