Skip to content

Commit 514ad74

Browse files
committed
Add tests for AppInsightsMetricExporter
1 parent bf2b2c6 commit 514ad74

File tree

4 files changed

+522
-0
lines changed

4 files changed

+522
-0
lines changed

src/WebJobs.Script/Diagnostics/ApplicationInsightsMetricExporter.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ public ApplicationInsightsMetricExporter(IOptions<ApplicationInsightsMetricExpor
5858
_listener.SetMeasurementEventCallback(CreateCallback<decimal>());
5959
}
6060

61+
/// <summary>
62+
/// Initializes this module, starting the meter listener and exporting process.
63+
/// </summary>
64+
/// <param name="configuration">The telemetry configuration.</param>
6165
public void Initialize(TelemetryConfiguration configuration)
6266
{
6367
ArgumentNullException.ThrowIfNull(configuration);
@@ -66,6 +70,7 @@ public void Initialize(TelemetryConfiguration configuration)
6670
_listener.Start();
6771
}
6872

73+
/// <inheritdoc />
6974
public async ValueTask DisposeAsync()
7075
{
7176
_listener.Dispose();
@@ -76,6 +81,14 @@ public async ValueTask DisposeAsync()
7681
_shutdown.Dispose();
7782
}
7883

84+
/// <summary>
85+
/// Flushes the internal client.
86+
/// </summary>
87+
/// <remarks>
88+
/// Primarily for testing purposes.
89+
/// </remarks>
90+
internal void Flush() => _client.Flush();
91+
7992
private static MeasurementCallback<T> CreateCallback<T>()
8093
where T : struct, INumber<T>, IConvertible
8194
{
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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.Diagnostics.Metrics;
8+
using AwesomeAssertions;
9+
using Microsoft.Azure.WebJobs.Script.Diagnostics;
10+
using Xunit;
11+
12+
namespace Microsoft.Azure.WebJobs.Script.Tests.Diagnostics
13+
{
14+
public class ApplicationInsightsMetricExporterOptionsTests
15+
{
16+
[Fact]
17+
public void ShouldListenTo_NullInstrument_Throws()
18+
{
19+
// arrange
20+
ApplicationInsightsMetricExporterOptions options = new();
21+
Instrument? instrument = null;
22+
23+
// act
24+
Action act = () => options.ShouldListenTo(instrument!);
25+
26+
// assert
27+
act.Should().Throw<ArgumentNullException>().WithParameterName("instrument");
28+
}
29+
30+
[Fact]
31+
public void ShouldListenTo_Empty_ReturnsFalse()
32+
{
33+
// arrange
34+
ApplicationInsightsMetricExporterOptions options = new();
35+
using Meter meter = new("test.meter");
36+
Counter<long> counter = meter.CreateCounter<long>("test.counter");
37+
38+
// act
39+
bool result = options.ShouldListenTo(counter);
40+
41+
// assert
42+
result.Should().BeFalse();
43+
}
44+
45+
[Fact]
46+
public void ShouldListenTo_MeterNotSet_ReturnsFalse()
47+
{
48+
// arrange
49+
ApplicationInsightsMetricExporterOptions options = new();
50+
options.Meters.Add("configured.meter");
51+
using Meter meter = new("different.meter");
52+
Counter<long> counter = meter.CreateCounter<long>("test.counter");
53+
54+
// act
55+
bool result = options.ShouldListenTo(counter);
56+
57+
// assert
58+
result.Should().BeFalse();
59+
}
60+
61+
[Fact]
62+
public void ShouldListenTo_MeterSet_ReturnsTrue()
63+
{
64+
// arrange
65+
ApplicationInsightsMetricExporterOptions options = new();
66+
options.Meters.Add("test.meter");
67+
using Meter meter = new("test.meter");
68+
Counter<long> counter = meter.CreateCounter<long>("test.counter");
69+
70+
// act
71+
bool result = options.ShouldListenTo(counter);
72+
73+
// assert
74+
result.Should().BeTrue();
75+
}
76+
77+
[Fact]
78+
public void ShouldListenTo_IsCaseSensitive()
79+
{
80+
// arrange
81+
ApplicationInsightsMetricExporterOptions options = new();
82+
options.Meters.Add("Test.Meter");
83+
using Meter meter = new("test.meter"); // different case
84+
Counter<long> counter = meter.CreateCounter<long>("test.counter");
85+
86+
// act
87+
bool result = options.ShouldListenTo(counter);
88+
89+
// assert
90+
result.Should().BeFalse();
91+
}
92+
93+
[Fact]
94+
public void ShouldListenTo_WorksWithDifferentInstrumentTypes()
95+
{
96+
// arrange
97+
ApplicationInsightsMetricExporterOptions options = new();
98+
options.Meters.Add("test.meter");
99+
using Meter meter = new("test.meter");
100+
Counter<long> counter = meter.CreateCounter<long>("test.counter");
101+
Histogram<double> histogram = meter.CreateHistogram<double>("test.histogram");
102+
ObservableGauge<int> gauge = meter.CreateObservableGauge<int>("test.gauge", () => 1);
103+
104+
// act & assert
105+
options.ShouldListenTo(counter).Should().BeTrue();
106+
options.ShouldListenTo(histogram).Should().BeTrue();
107+
options.ShouldListenTo(gauge).Should().BeTrue();
108+
}
109+
110+
[Fact]
111+
public void ShouldListenTo_HandlesMultipleConfiguredMeters()
112+
{
113+
// arrange
114+
ApplicationInsightsMetricExporterOptions options = new();
115+
options.Meters.Add("meter1");
116+
options.Meters.Add("meter2");
117+
118+
using Meter meter1 = new("meter1");
119+
using Meter meter2 = new("meter2");
120+
using Meter meter3 = new("meter3");
121+
122+
Counter<long> counter1 = meter1.CreateCounter<long>("counter1");
123+
Counter<long> counter2 = meter2.CreateCounter<long>("counter2");
124+
Counter<long> counter3 = meter3.CreateCounter<long>("counter3");
125+
126+
// act & assert
127+
options.ShouldListenTo(counter1).Should().BeTrue();
128+
options.ShouldListenTo(counter2).Should().BeTrue();
129+
options.ShouldListenTo(counter3).Should().BeFalse();
130+
}
131+
}
132+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
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

Comments
 (0)