Skip to content

Commit a29d1bf

Browse files
Merge pull request #3082 from mattsains-msft/sdkstats-features
Report usage of Track* APIs using sdkstats feature metric
2 parents e1bb83d + f3a520b commit a29d1bf

File tree

18 files changed

+645
-23
lines changed

18 files changed

+645
-23
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using System.Diagnostics;
2+
3+
namespace Microsoft.ApplicationInsights
4+
{
5+
using Extensibility.Implementation;
6+
using Microsoft.ApplicationInsights.Channel;
7+
using Microsoft.ApplicationInsights.DataContracts;
8+
using Microsoft.ApplicationInsights.Extensibility;
9+
using Microsoft.ApplicationInsights.Internal;
10+
using Microsoft.ApplicationInsights.Tests;
11+
using OpenTelemetry;
12+
using OpenTelemetry.Logs;
13+
using OpenTelemetry.Trace;
14+
using System;
15+
using System.Collections.Generic;
16+
using System.Threading;
17+
using System.Threading.Tasks;
18+
using Xunit;
19+
using System.Linq;
20+
21+
public class FeatureMetricEmissionHelperTests
22+
{
23+
24+
[Fact]
25+
public void GetOrCreate_SharesInstances()
26+
{
27+
using var helper1 = FeatureMetricEmissionHelper.GetOrCreate("a", "b");
28+
using var helper2 = FeatureMetricEmissionHelper.GetOrCreate("a", "b");
29+
using var helper3 = FeatureMetricEmissionHelper.GetOrCreate("different", "b");
30+
31+
Assert.Same(helper1, helper2);
32+
Assert.NotSame(helper1, helper3);
33+
}
34+
35+
[Fact]
36+
public void ReportsFeaturesSeen()
37+
{
38+
using var helper1 = FeatureMetricEmissionHelper.GetOrCreate("a", "b");
39+
40+
var metric = helper1.GetFeatureStatsbeat();
41+
42+
// no features reported should not emit metric
43+
Assert.Equal(0, metric.Value);
44+
Assert.Empty(metric.Tags.ToArray());
45+
46+
helper1.MarkFeatureInUse(StatsbeatFeatures.TrackEvent);
47+
helper1.MarkFeatureInUse(StatsbeatFeatures.TrackDependency);
48+
49+
metric = helper1.GetFeatureStatsbeat();
50+
var tags = metric.Tags.ToArray().ToDictionary(v => v.Key, v => v.Value);
51+
52+
Assert.Equal(1, metric.Value);
53+
AssertKey("rp", "unknown", tags);
54+
AssertKey("attach", "Manual", tags);
55+
AssertKey("cikey", "a", tags);
56+
AssertKey("feature", (ulong)1 + 32, tags); // == TrackEvent + TrackDependency
57+
AssertKey("type", 0, tags); // == feature
58+
Assert.Contains("os", tags);
59+
AssertKey("language", "dotnet", tags);
60+
AssertKey("product", "appinsights", tags);
61+
AssertKey("version", "b", tags);
62+
}
63+
64+
private void AssertKey<V, T>(string key, T value, IDictionary<string, V> dictionary) where T: V
65+
{
66+
Assert.Contains(key, dictionary);
67+
Assert.Equal(value, dictionary[key]);
68+
}
69+
}
70+
}

BASE/Test/Microsoft.ApplicationInsights.Test/Microsoft.ApplicationInsights.Tests/TelemetryClientTest.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ public void Dispose()
5555
this.telemetryClient?.TelemetryConfiguration?.Dispose();
5656
}
5757

58+
[Fact]
59+
public void TelemetryClientInitializesFeatureReporter()
60+
{
61+
Assert.NotNull(this.telemetryClient.Configuration.FeatureReporter);
62+
}
63+
5864
#region TrackEvent
5965

6066
[Fact]

BASE/src/Microsoft.ApplicationInsights/Extensibility/TelemetryConfiguration.cs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Microsoft.ApplicationInsights.DataContracts;
1313
using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing;
1414
using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing.SelfDiagnostics;
15+
using Microsoft.ApplicationInsights.Internal;
1516
using Microsoft.ApplicationInsights.Metrics;
1617
using Microsoft.Extensions.DependencyInjection;
1718
using Microsoft.Extensions.Hosting;
@@ -31,6 +32,9 @@ public sealed class TelemetryConfiguration : IDisposable
3132

3233
internal const string ApplicationInsightsActivitySourceName = "Microsoft.ApplicationInsights";
3334
internal const string ApplicationInsightsMeterName = "Microsoft.ApplicationInsights";
35+
36+
internal FeatureMetricEmissionHelper FeatureReporter;
37+
3438
private static readonly Lazy<TelemetryConfiguration> DefaultInstance =
3539
new Lazy<TelemetryConfiguration>(() => new TelemetryConfiguration(), LazyThreadSafetyMode.ExecutionAndPublication);
3640

@@ -49,6 +53,7 @@ public sealed class TelemetryConfiguration : IDisposable
4953
private bool? disableOfflineStorage;
5054
private bool? enableLiveMetrics;
5155
private bool? enableTraceBasedLogsSampler;
56+
private string extensionVersion = "sha" + VersionUtils.GetVersion(typeof(TelemetryClient));
5257

5358
private Action<IOpenTelemetryBuilder> builderConfiguration;
5459
private OpenTelemetrySdk openTelemetrySdk;
@@ -206,6 +211,19 @@ public bool? EnableTraceBasedLogsSampler
206211
}
207212
}
208213

214+
/// <summary>
215+
/// Gets or sets a value indicating the version string to report to SDK stats. Eg., "shc1.2.3".
216+
/// </summary>
217+
internal string ExtensionVersion
218+
{
219+
get => this.extensionVersion;
220+
set
221+
{
222+
this.ThrowIfBuilt();
223+
this.extensionVersion = value;
224+
}
225+
}
226+
209227
/// <summary>
210228
/// Gets the default ActivitySource used by TelemetryClient.
211229
/// </summary>
@@ -331,6 +349,14 @@ internal void SetCloudRole(string serviceName, string serviceInstanceId = null,
331349
});
332350
}
333351

352+
internal FeatureMetricEmissionHelper InitializeFeatureReporter()
353+
{
354+
var connectionString = Microsoft.ApplicationInsights.Internal.ConnectionString.Parse(this.ConnectionString);
355+
var ciKey = connectionString.GetNonRequired("InstrumentationKey");
356+
this.FeatureReporter = FeatureMetricEmissionHelper.GetOrCreate(ciKey, this.extensionVersion);
357+
return this.FeatureReporter;
358+
}
359+
334360
/// <summary>
335361
/// Builds the OpenTelemetry SDK with the current configuration.
336362
/// This is called internally by TelemetryClient and should not be called directly.
@@ -457,11 +483,9 @@ private void Dispose(bool disposing)
457483
// - All registered processors, exporters, etc.
458484
this.openTelemetrySdk?.Dispose();
459485

460-
// Dispose the ActivitySource
461486
this.defaultActivitySource?.Dispose();
462-
463-
// Dispose the MetricsManager
464487
this.metricsManager?.Dispose();
488+
this.FeatureReporter?.Dispose();
465489

466490
this.isDisposed = true;
467491

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
// This has been copied from https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/src/Shared/ConnectionString.cs
4+
5+
#nullable enable
6+
7+
namespace Microsoft.ApplicationInsights.Internal
8+
{
9+
using System;
10+
using System.Collections.Generic;
11+
using System.Text;
12+
13+
internal sealed class ConnectionString
14+
{
15+
private readonly Dictionary<string, string> pairs;
16+
private readonly string pairSeparator;
17+
private readonly string keywordValueSeparator;
18+
19+
private ConnectionString(Dictionary<string, string> pairs, string pairSeparator, string keywordValueSeparator)
20+
{
21+
this.pairs = pairs;
22+
this.pairSeparator = pairSeparator;
23+
this.keywordValueSeparator = keywordValueSeparator;
24+
}
25+
26+
public static ConnectionString Parse(string connectionString, string segmentSeparator = ";", string keywordValueSeparator = "=", bool allowEmptyValues = false)
27+
{
28+
Validate(connectionString, segmentSeparator, keywordValueSeparator, allowEmptyValues);
29+
return new ConnectionString(ParseSegments(connectionString, segmentSeparator, keywordValueSeparator), segmentSeparator, keywordValueSeparator);
30+
}
31+
32+
public static ConnectionString Empty(string segmentSeparator = ";", string keywordValueSeparator = "=") =>
33+
new (new Dictionary<string, string>(), segmentSeparator, keywordValueSeparator);
34+
35+
public string GetRequired(string keyword) =>
36+
this.pairs.TryGetValue(keyword, out var value) ? value : throw new InvalidOperationException($"Required keyword '{keyword}' is missing in connection string.");
37+
38+
public string? GetNonRequired(string keyword) =>
39+
this.pairs.TryGetValue(keyword, out var value) ? value : null;
40+
41+
public bool TryGetSegmentValue(string keyword, out string? value) =>
42+
this.pairs.TryGetValue(keyword, out value);
43+
44+
public string? GetSegmentValueOrDefault(string keyword, string defaultValue) =>
45+
this.pairs.TryGetValue(keyword, out var value) switch {
46+
false => defaultValue,
47+
true => value
48+
};
49+
50+
public bool ContainsSegmentKey(string keyword) =>
51+
this.pairs.ContainsKey(keyword);
52+
53+
public void Replace(string keyword, string value)
54+
{
55+
if (this.pairs.ContainsKey(keyword))
56+
{
57+
this.pairs[keyword] = value;
58+
}
59+
}
60+
61+
public void Add(string keyword, string value) =>
62+
this.pairs.Add(keyword, value);
63+
64+
public override string ToString()
65+
{
66+
if (this.pairs.Count == 0)
67+
{
68+
return string.Empty;
69+
}
70+
71+
var stringBuilder = new StringBuilder();
72+
var isFirst = true;
73+
foreach (KeyValuePair<string, string> pair in this.pairs)
74+
{
75+
if (isFirst)
76+
{
77+
isFirst = false;
78+
}
79+
else
80+
{
81+
stringBuilder.Append(this.pairSeparator);
82+
}
83+
84+
stringBuilder.Append(pair.Key);
85+
if (pair.Value != null)
86+
{
87+
stringBuilder.Append(this.keywordValueSeparator).Append(pair.Value);
88+
}
89+
}
90+
91+
return stringBuilder.ToString();
92+
}
93+
94+
private static Dictionary<string, string> ParseSegments(in string connectionString, in string separator, in string keywordValueSeparator)
95+
{
96+
var pairs = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
97+
98+
var segmentStart = -1;
99+
var segmentEnd = 0;
100+
101+
while (TryGetNextSegment(connectionString, separator, ref segmentStart, ref segmentEnd))
102+
{
103+
var kvSeparatorIndex = connectionString.IndexOf(keywordValueSeparator, segmentStart, segmentEnd - segmentStart, StringComparison.Ordinal);
104+
int keywordStart = GetStart(connectionString, segmentStart);
105+
int keyLength = GetLength(connectionString, keywordStart, kvSeparatorIndex);
106+
107+
var keyword = connectionString.Substring(keywordStart, keyLength);
108+
if (pairs.ContainsKey(keyword))
109+
{
110+
throw new InvalidOperationException($"Duplicated keyword '{keyword}'");
111+
}
112+
113+
var valueStart = GetStart(connectionString, kvSeparatorIndex + keywordValueSeparator.Length);
114+
var valueLength = GetLength(connectionString, valueStart, segmentEnd);
115+
pairs.Add(keyword, connectionString.Substring(valueStart, valueLength));
116+
}
117+
118+
return pairs;
119+
120+
static int GetStart(in string str, int start)
121+
{
122+
while (start < str.Length && char.IsWhiteSpace(str[start]))
123+
{
124+
start++;
125+
}
126+
127+
return start;
128+
}
129+
130+
static int GetLength(in string str, in int start, int end)
131+
{
132+
while (end > start && char.IsWhiteSpace(str[end - 1]))
133+
{
134+
end--;
135+
}
136+
137+
return end - start;
138+
}
139+
}
140+
141+
private static bool TryGetNextSegment(in string str, in string separator, ref int start, ref int end)
142+
{
143+
if (start == -1)
144+
{
145+
start = 0;
146+
}
147+
else
148+
{
149+
start = end + separator.Length;
150+
if (start >= str.Length)
151+
{
152+
return false;
153+
}
154+
}
155+
156+
end = str.IndexOf(separator, start, StringComparison.Ordinal);
157+
if (end == -1)
158+
{
159+
end = str.Length;
160+
}
161+
162+
return true;
163+
}
164+
165+
private static void Validate(string connectionString, string segmentSeparator, string keywordValueSeparator, bool allowEmptyValues)
166+
{
167+
var segmentStart = -1;
168+
var segmentEnd = 0;
169+
170+
while (TryGetNextSegment(connectionString, segmentSeparator, ref segmentStart, ref segmentEnd))
171+
{
172+
if (segmentStart == segmentEnd)
173+
{
174+
if (segmentStart == 0)
175+
{
176+
throw new InvalidOperationException($"Connection string starts with separator '{segmentSeparator}'.");
177+
}
178+
179+
throw new InvalidOperationException($"Connection string contains two following separators '{segmentSeparator}'.");
180+
}
181+
182+
var kvSeparatorIndex = connectionString.IndexOf(keywordValueSeparator, segmentStart, segmentEnd - segmentStart, StringComparison.Ordinal);
183+
if (kvSeparatorIndex == -1)
184+
{
185+
throw new InvalidOperationException($"Connection string doesn't have value for keyword '{connectionString.Substring(segmentStart, segmentEnd - segmentStart)}'.");
186+
}
187+
188+
if (segmentStart == kvSeparatorIndex)
189+
{
190+
throw new InvalidOperationException($"Connection string has value '{connectionString.Substring(segmentStart, kvSeparatorIndex - segmentStart)}' with no keyword.");
191+
}
192+
193+
if (!allowEmptyValues && kvSeparatorIndex + 1 == segmentEnd)
194+
{
195+
throw new InvalidOperationException($"Connection string has keyword '{connectionString.Substring(segmentStart, kvSeparatorIndex - segmentStart)}' with empty value.");
196+
}
197+
}
198+
}
199+
}
200+
}

0 commit comments

Comments
 (0)