microsoft/teams.net

Public

mirrored from https://github.com/microsoft/teams.netAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
fix/msal-agentic-cache

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

core/test/Microsoft.Teams.Core.UnitTests/Diagnostics/TelemetryTests.cs

262lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System.Diagnostics;
5using System.Diagnostics.Metrics;
6using System.Net;
7using System.Text;
8using Microsoft.AspNetCore.Http;
9using Microsoft.Extensions.Configuration;
10using Microsoft.Extensions.Logging.Abstractions;
11using Microsoft.Teams.Core.Diagnostics;
12using Microsoft.Teams.Core.Schema;
13using Moq;
14using Moq.Protected;
15
16namespace Microsoft.Teams.Core.UnitTests.Diagnostics;
17
18public class TelemetryTests
19{
20 [Fact]
21 public void CoreTelemetryNames_ConstantsHaveExpectedValues()
22 {
23 Assert.Equal("Microsoft.Teams.Core", CoreTelemetryNames.ActivitySourceName);
24 Assert.Equal("Microsoft.Teams.Core", CoreTelemetryNames.MeterName);
25 }
26
27 [Fact]
28 public async Task ProcessAsync_EmitsTurnSpanWithExpectedTags()
29 {
30 using SpanCapture capture = new();
31
32 BotApplication botApp = CreateBotApplication();
33 botApp.OnActivity = (_, _) => Task.CompletedTask;
34
35 CoreActivity activity = new()
36 {
37 Type = ActivityType.Message,
38 Id = "act-1",
39 ChannelId = "msteams",
40 ServiceUrl = new Uri("https://smba.example/"),
41 Conversation = new("conv-1"),
42 };
43
44 await botApp.ProcessAsync(BuildHttpContext(activity));
45
46 Activity turn = Assert.Single(capture.Stopped, a => a.OperationName == "turn");
47 Assert.Equal("act-1", turn.GetTagItem("activity.id"));
48 Assert.Equal("msteams", turn.GetTagItem("channel.id"));
49 Assert.Equal("conv-1", turn.GetTagItem("conversation.id"));
50 Assert.Equal("https://smba.example/", turn.GetTagItem("service.url"));
51 Assert.Equal(ActivityStatusCode.Unset, turn.Status);
52 }
53
54 [Fact]
55 public async Task ProcessAsync_NestsMiddlewareSpansUnderTurn()
56 {
57 using SpanCapture capture = new();
58
59 BotApplication botApp = CreateBotApplication();
60 Mock<ITurnMiddleware> mw = new();
61 mw.Setup(m => m.OnTurnAsync(It.IsAny<BotApplication>(), It.IsAny<CoreActivity>(), It.IsAny<NextTurn>(), It.IsAny<CancellationToken>()))
62 .Returns<BotApplication, CoreActivity, NextTurn, CancellationToken>((_, _, next, ct) => next(ct));
63 botApp.UseMiddleware(mw.Object);
64 botApp.OnActivity = (_, _) => Task.CompletedTask;
65
66 await botApp.ProcessAsync(BuildHttpContext(NewActivity()));
67
68 Activity turn = Assert.Single(capture.Stopped, a => a.OperationName == "turn");
69 Activity middleware = Assert.Single(capture.Stopped, a => a.OperationName == "middleware");
70 Assert.Equal(turn.SpanId, middleware.ParentSpanId);
71 Assert.Equal(0, middleware.GetTagItem("middleware.index"));
72 Assert.NotNull(middleware.GetTagItem("middleware.name"));
73 }
74
75 [Fact]
76 public async Task ProcessAsync_RecordsExceptionOnTurnSpanAndIncrementsErrorCounter()
77 {
78 using SpanCapture spanCapture = new();
79 using MetricCapture metricCapture = new();
80
81 BotApplication botApp = CreateBotApplication();
82 botApp.OnActivity = (_, _) => throw new InvalidOperationException("boom");
83
84 await Assert.ThrowsAsync<BotHandlerException>(() =>
85 botApp.ProcessAsync(BuildHttpContext(NewActivity())));
86
87 Activity turn = Assert.Single(spanCapture.Stopped, a => a.OperationName == "turn");
88 Assert.Equal(ActivityStatusCode.Error, turn.Status);
89 Assert.Contains(turn.Events, e => e.Name == "exception");
90
91 Assert.True(metricCapture.GetCounterTotal("teams.handler.errors") >= 1);
92 Assert.True(metricCapture.GetCounterTotal("teams.activities.received") >= 1);
93 Assert.True(metricCapture.HistogramSampleCount("teams.turn.duration") >= 1);
94 }
95
96 [Fact]
97 public async Task ConversationClient_SendActivityAsync_EmitsConversationClientSpanAndOutboundCallsCounter()
98 {
99 using SpanCapture spanCapture = new();
100 using MetricCapture metricCapture = new();
101
102 Mock<HttpMessageHandler> handler = new();
103 handler.Protected()
104 .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
105 .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK)
106 {
107 Content = new StringContent("{\"id\":\"sent-1\"}"),
108 });
109
110 ConversationClient client = new(new HttpClient(handler.Object));
111
112 SendActivityResponse? response = await client.SendActivityAsync(new CoreActivity
113 {
114 Type = ActivityType.Message,
115 ServiceUrl = new Uri("https://smba.example/"),
116 Conversation = new("conv-1"),
117 });
118
119 Assert.NotNull(response);
120 Activity span = Assert.Single(spanCapture.Stopped, a => a.OperationName == "conversation_client");
121 Assert.Equal("sendActivity", span.GetTagItem("operation"));
122 Assert.Equal("conv-1", span.GetTagItem("conversation.id"));
123 Assert.Equal("sent-1", span.GetTagItem("activity.id"));
124
125 Assert.Equal(1, metricCapture.GetCounterTotal("teams.outbound.calls"));
126 Assert.Equal(0, metricCapture.GetCounterTotal("teams.outbound.errors"));
127 }
128
129 [Fact]
130 public async Task ConversationClient_SendActivityAsync_RecordsErrorOnFailure()
131 {
132 using SpanCapture spanCapture = new();
133 using MetricCapture metricCapture = new();
134
135 Mock<HttpMessageHandler> handler = new();
136 handler.Protected()
137 .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
138 .ThrowsAsync(new HttpRequestException("network down"));
139
140 ConversationClient client = new(new HttpClient(handler.Object));
141
142 await Assert.ThrowsAsync<HttpRequestException>(() => client.SendActivityAsync(new CoreActivity
143 {
144 Type = ActivityType.Message,
145 ServiceUrl = new Uri("https://smba.example/"),
146 Conversation = new("conv-1"),
147 }));
148
149 Activity span = Assert.Single(spanCapture.Stopped, a => a.OperationName == "conversation_client");
150 Assert.Equal(ActivityStatusCode.Error, span.Status);
151 Assert.Equal(1, metricCapture.GetCounterTotal("teams.outbound.errors"));
152 Assert.Equal(0, metricCapture.GetCounterTotal("teams.outbound.calls"));
153 }
154
155 private static CoreActivity NewActivity() => new()
156 {
157 Type = ActivityType.Message,
158 Id = "act-test",
159 ChannelId = "msteams",
160 ServiceUrl = new Uri("https://smba.example/"),
161 Conversation = new("conv-test"),
162 };
163
164 private static DefaultHttpContext BuildHttpContext(CoreActivity activity)
165 {
166 DefaultHttpContext ctx = new();
167 ctx.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(activity.ToJson()));
168 ctx.Request.ContentType = "application/json";
169 return ctx;
170 }
171
172 private static BotApplication CreateBotApplication()
173 {
174 ConversationClient cc = new(new HttpClient(Mock.Of<HttpMessageHandler>()));
175 UserTokenClient ut = new(new HttpClient(Mock.Of<HttpMessageHandler>()), Mock.Of<IConfiguration>(), NullLogger<UserTokenClient>.Instance);
176 return new BotApplication(cc, ut, NullLogger<BotApplication>.Instance);
177 }
178
179 /// <summary>
180 /// Test harness: subscribes an <see cref="ActivityListener"/> to the SDK's source and records every span.
181 /// </summary>
182 private sealed class SpanCapture : IDisposable
183 {
184 private readonly ActivityListener _listener;
185 public List<Activity> Stopped { get; } = [];
186
187 public SpanCapture()
188 {
189 _listener = new ActivityListener
190 {
191 ShouldListenTo = src => src.Name == CoreTelemetryNames.ActivitySourceName,
192 Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
193 ActivityStopped = a =>
194 {
195 lock (Stopped) { Stopped.Add(a); }
196 },
197 };
198 ActivitySource.AddActivityListener(_listener);
199 }
200
201 public void Dispose() => _listener.Dispose();
202 }
203
204 /// <summary>
205 /// Test harness: subscribes a <see cref="MeterListener"/> to the SDK's meter and aggregates emitted measurements.
206 /// </summary>
207 private sealed class MetricCapture : IDisposable
208 {
209 private readonly MeterListener _listener;
210 private readonly Dictionary<string, long> _counterTotals = new(StringComparer.Ordinal);
211 private readonly Dictionary<string, int> _histogramSamples = new(StringComparer.Ordinal);
212
213 public MetricCapture()
214 {
215 _listener = new MeterListener
216 {
217 InstrumentPublished = (instrument, listener) =>
218 {
219 if (instrument.Meter.Name == CoreTelemetryNames.MeterName)
220 {
221 listener.EnableMeasurementEvents(instrument);
222 }
223 },
224 };
225 _listener.SetMeasurementEventCallback<long>((instrument, value, _, _) =>
226 {
227 lock (_counterTotals)
228 {
229 _counterTotals.TryGetValue(instrument.Name, out long total);
230 _counterTotals[instrument.Name] = total + value;
231 }
232 });
233 _listener.SetMeasurementEventCallback<double>((instrument, _, _, _) =>
234 {
235 lock (_histogramSamples)
236 {
237 _histogramSamples.TryGetValue(instrument.Name, out int count);
238 _histogramSamples[instrument.Name] = count + 1;
239 }
240 });
241 _listener.Start();
242 }
243
244 public long GetCounterTotal(string name)
245 {
246 lock (_counterTotals)
247 {
248 return _counterTotals.TryGetValue(name, out long total) ? total : 0;
249 }
250 }
251
252 public int HistogramSampleCount(string name)
253 {
254 lock (_histogramSamples)
255 {
256 return _histogramSamples.TryGetValue(name, out int count) ? count : 0;
257 }
258 }
259
260 public void Dispose() => _listener.Dispose();
261 }
262}
263