Skip to content

Commit b8e346f

Browse files
Add AppInsights (#196)
* initial * Cleanup * opt out + cleanup * Remove extra stuff * more cleanup * Undo unneeded changes * Add README * Add back in connection string setting * PR comments
1 parent 80ef52c commit b8e346f

13 files changed

+421
-3
lines changed

.editorconfig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ root = true
77
[*.cs]
88

99
dotnet_analyzer_diagnostic.severity = error
10+
# Namespace does not match folder structure - Ideally this should be enabled but it seems to have issues with root level files so disabling for now
11+
dotnet_diagnostic.IDE0130.severity = none
1012

1113
# Documentation related errors, remove once they are fixed
1214
dotnet_diagnostic.CS1591.severity = none

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Further information on the Azure SQL binding for Azure Functions is also availab
3939
- [Single Row](#single-row)
4040
- [Primary Keys and Identity Columns](#primary-keys-and-identity-columns)
4141
- [Known Issues](#known-issues)
42+
- [Telemetry](#telemetry)
4243
- [Trademarks](#trademarks)
4344

4445
## Quick Start
@@ -526,6 +527,10 @@ This changes if one of the primary key columns is an identity column though. In
526527
- Output bindings against tables with columns of data types `NTEXT`, `TEXT`, or `IMAGE` are not supported and data upserts will fail. These types [will be removed](https://docs.microsoft.com/sql/t-sql/data-types/ntext-text-and-image-transact-sql) in a future version of SQL Server and are not compatible with the `OPENJSON` function used by this Azure Functions binding.
527528
- Case-sensitive [collations](https://docs.microsoft.com/sql/relational-databases/collations/collation-and-unicode-support#Collation_Defn) are not currently supported. This functionality will be added in a future release. [#133](https://github.com/Azure/azure-functions-sql-extension/issues/133) tracks progress on this issue.
528529
530+
## Telemetry
531+
532+
This extension collect usage data in order to help us improve your experience. The data is anonymous and doesn't include any personal information. You can opt-out of telemetry by setting the `AZUREFUNCTIONS_SQLBINDINGS_TELEMETRY_OPTOUT` environment variable or the `AzureFunctionsSqlBindingsTelemetryOptOut` app setting (in your `*.settings.json` file) to '1', 'true' or 'yes';
533+
529534
## Trademarks
530535
531536
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft’s Trademark & Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party’s policies.

builds/azure-pipelines/template-steps-build-test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ steps:
191191
env:
192192
TEST_SERVER: '$(testServer)'
193193
NODE_MODULES_PATH: '$(nodeModulesPath)'
194+
AZUREFUNCTIONS_SQLBINDINGS_TELEMETRY_OPTOUT: '1'
194195
inputs:
195196
command: test
196197
projects: '${{ parameters.solution }}'
@@ -201,6 +202,7 @@ steps:
201202
displayName: '.NET Test on Linux'
202203
env:
203204
SA_PASSWORD: '$(serverPassword)'
205+
AZUREFUNCTIONS_SQLBINDINGS_TELEMETRY_OPTOUT: '1'
204206
inputs:
205207
command: test
206208
projects: '${{ parameters.solution }}'

samples/local.settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
55
"FUNCTIONS_WORKER_RUNTIME": "dotnet",
66
"SqlConnectionString": ""
7-
}
7+
}
88
}

src/Microsoft.Azure.WebJobs.Extensions.Sql.csproj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,12 @@
1818
<PackageLicenseExpression>MIT</PackageLicenseExpression>
1919
<PackageProjectUrl>https://github.com/Azure/azure-functions-sql-extension</PackageProjectUrl>
2020
<PackageIcon>pkgicon.png</PackageIcon>
21-
<!-- Need to set root namespace to empty for IDE0130 to work properly - otherwise it errors out on top-level namespaces for some reason -->
22-
<RootNamespace></RootNamespace>
2321
<PublishRepositoryUrl>true</PublishRepositoryUrl>
2422
<EmbedUntrackedSources>true</EmbedUntrackedSources>
2523
</PropertyGroup>
2624

2725
<ItemGroup>
26+
<PackageReference Include="Microsoft.ApplicationInsights" Version="2.19.0" />
2827
<PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.*" />
2928
<PackageReference Include="Microsoft.Data.SqlClient" Version="3.0.*" />
3029
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.*" PrivateAssets="All" />

src/SqlBindingConfigProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public void Initialize(ExtensionConfigContext context)
4545
{
4646
throw new ArgumentNullException(nameof(context));
4747
}
48+
Telemetry.Telemetry.Instance.Initialize(this._configuration, this._loggerFactory);
4849
#pragma warning disable CS0618 // Fine to use this for our stuff
4950
FluentBindingRule<SqlAttribute> inputOutputRule = context.AddBindingRule<SqlAttribute>();
5051
var converter = new SqlConverter(this._configuration);

src/Telemetry/Telemetry.cs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Runtime.InteropServices;
8+
using System.Threading.Tasks;
9+
using Microsoft.Extensions.Logging;
10+
using Microsoft.ApplicationInsights;
11+
using Microsoft.ApplicationInsights.Extensibility;
12+
using Microsoft.Extensions.Configuration;
13+
using Microsoft.Azure.WebJobs.Logging;
14+
15+
namespace Microsoft.Azure.WebJobs.Extensions.Sql.Telemetry
16+
{
17+
public sealed class Telemetry
18+
{
19+
internal static Telemetry Instance = new Telemetry();
20+
21+
private const string EventsNamespace = "azure-functions-sql-bindings";
22+
internal static string CurrentSessionId;
23+
private TelemetryClient _client;
24+
private Dictionary<string, string> _commonProperties;
25+
private Dictionary<string, double> _commonMeasurements;
26+
private Task _trackEventTask;
27+
private ILogger _logger;
28+
private bool _initialized;
29+
private const string InstrumentationKey = "98697a1c-1416-486a-99ac-c6c74ebe5ebd";
30+
/// <summary>
31+
/// The environment variable used for opting out of telemetry
32+
/// </summary>
33+
public const string TelemetryOptoutEnvVar = "AZUREFUNCTIONS_SQLBINDINGS_TELEMETRY_OPTOUT";
34+
/// <summary>
35+
/// The app setting used for opting out of telemetry
36+
/// </summary>
37+
public const string TelemetryOptoutSetting = "AzureFunctionsSqlBindingsTelemetryOptOut";
38+
39+
public const string WelcomeMessage = @"Azure SQL binding for Azure Functions
40+
---------------------
41+
Telemetry
42+
---------
43+
This extension collect usage data in order to help us improve your experience. The data is anonymous and doesn't include any personal information. You can opt-out of telemetry by setting the " + TelemetryOptoutEnvVar + " environment variable or the " + TelemetryOptoutSetting + @" + app setting to '1', 'true' or 'yes';
44+
";
45+
46+
public void Initialize(IConfiguration config, ILoggerFactory loggerFactory)
47+
{
48+
this._logger = loggerFactory.CreateLogger(LogCategories.Bindings);
49+
this.Enabled = !(Utils.GetEnvironmentVariableAsBool(TelemetryOptoutEnvVar) || Utils.GetConfigSettingAsBool(TelemetryOptoutSetting, config));
50+
if (!this.Enabled)
51+
{
52+
this._logger.LogInformation("Telemetry disabled");
53+
return;
54+
}
55+
this._logger.LogInformation(WelcomeMessage);
56+
// Store the session ID in a static field so that it can be reused
57+
CurrentSessionId = Guid.NewGuid().ToString();
58+
59+
string productVersion = typeof(Telemetry).Assembly.GetName().Version.ToString();
60+
//initialize in task to offload to parallel thread
61+
this._trackEventTask = Task.Factory.StartNew(() => this.InitializeTelemetry(productVersion));
62+
this._initialized = true;
63+
}
64+
65+
public bool Enabled { get; private set; }
66+
67+
public void TrackEvent(string eventName, IDictionary<string, string> properties,
68+
IDictionary<string, double> measurements)
69+
{
70+
if (!this._initialized || !this.Enabled)
71+
{
72+
return;
73+
}
74+
this._logger.LogInformation($"Sending event {eventName}");
75+
76+
//continue task in existing parallel thread
77+
this._trackEventTask = this._trackEventTask.ContinueWith(
78+
x => this.TrackEventTask(eventName, properties, measurements)
79+
);
80+
}
81+
82+
private void InitializeTelemetry(string productVersion)
83+
{
84+
try
85+
{
86+
var config = new TelemetryConfiguration(InstrumentationKey);
87+
this._client = new TelemetryClient(config);
88+
this._client.Context.Session.Id = CurrentSessionId;
89+
this._client.Context.Device.OperatingSystem = RuntimeInformation.OSDescription;
90+
91+
this._commonProperties = new TelemetryCommonProperties(productVersion).GetTelemetryCommonProperties();
92+
this._commonMeasurements = new Dictionary<string, double>();
93+
}
94+
catch (Exception e)
95+
{
96+
this._client = null;
97+
// we don't want to fail the tool if telemetry fails.
98+
Debug.Fail(e.ToString());
99+
}
100+
}
101+
102+
private void TrackEventTask(
103+
string eventName,
104+
IDictionary<string, string> properties,
105+
IDictionary<string, double> measurements)
106+
{
107+
if (this._client is null)
108+
{
109+
return;
110+
}
111+
112+
try
113+
{
114+
Dictionary<string, string> eventProperties = this.GetEventProperties(properties);
115+
Dictionary<string, double> eventMeasurements = this.GetEventMeasures(measurements);
116+
117+
this._client.TrackEvent($"{EventsNamespace}/{eventName}", eventProperties, eventMeasurements);
118+
this._client.Flush();
119+
}
120+
catch (Exception e)
121+
{
122+
Debug.Fail(e.ToString());
123+
}
124+
}
125+
126+
private Dictionary<string, double> GetEventMeasures(IDictionary<string, double> measurements)
127+
{
128+
var eventMeasurements = new Dictionary<string, double>(this._commonMeasurements);
129+
if (measurements != null)
130+
{
131+
foreach (KeyValuePair<string, double> measurement in measurements)
132+
{
133+
eventMeasurements[measurement.Key] = measurement.Value;
134+
}
135+
}
136+
return eventMeasurements;
137+
}
138+
139+
private Dictionary<string, string> GetEventProperties(IDictionary<string, string> properties)
140+
{
141+
if (properties != null)
142+
{
143+
var eventProperties = new Dictionary<string, string>(this._commonProperties);
144+
foreach (KeyValuePair<string, string> property in properties)
145+
{
146+
eventProperties[property.Key] = property.Value;
147+
}
148+
return eventProperties;
149+
}
150+
else
151+
{
152+
return this._commonProperties;
153+
}
154+
}
155+
}
156+
}
157+
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using System.Runtime.InteropServices;
6+
7+
namespace Microsoft.Azure.WebJobs.Extensions.Sql.Telemetry
8+
{
9+
public class TelemetryCommonProperties
10+
{
11+
private readonly string _productVersion;
12+
13+
public TelemetryCommonProperties(
14+
string productVersion)
15+
{
16+
this._productVersion = productVersion;
17+
}
18+
19+
private const string OSVersion = "OSVersion";
20+
private const string ProductVersion = "ProductVersion";
21+
22+
public Dictionary<string, string> GetTelemetryCommonProperties()
23+
{
24+
return new Dictionary<string, string>
25+
{
26+
{OSVersion, RuntimeInformation.OSDescription},
27+
{ProductVersion, this._productVersion}
28+
};
29+
}
30+
}
31+
}
32+

src/Telemetry/TelemetryUtils.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using Microsoft.Data.SqlClient;
6+
7+
namespace Microsoft.Azure.WebJobs.Extensions.Sql.Telemetry
8+
{
9+
public static class TelemetryUtils
10+
{
11+
/// <summary>
12+
/// Adds common connection properties to the property bag for a telemetry event.
13+
/// </summary>
14+
/// <param name="props">The property bag to add our connection properties to</param>
15+
/// <param name="conn">The connection to add properties of</param>
16+
public static void AddConnectionProps(this Dictionary<string, string> props, SqlConnection conn)
17+
{
18+
props.Add(nameof(SqlConnection.ServerVersion), conn.ServerVersion);
19+
}
20+
}
21+
}

src/Utils.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.Extensions.Configuration;
6+
7+
namespace Microsoft.Azure.WebJobs.Extensions.Sql
8+
{
9+
public static class Utils
10+
{
11+
/// <summary>
12+
/// Gets the specified environment variable and converts it to a boolean.
13+
/// </summary>
14+
/// <param name="name">Name of the environment variable</param>
15+
/// <param name="defaultValue">Value to use if the variable doesn't exist or is unable to be parsed</param>
16+
/// <returns>True if the variable exists and is set to a value that can be parsed as true, false otherwise</returns>
17+
public static bool GetEnvironmentVariableAsBool(string name, bool defaultValue = false)
18+
{
19+
string str = Environment.GetEnvironmentVariable(name);
20+
if (string.IsNullOrEmpty(str))
21+
{
22+
return defaultValue;
23+
}
24+
25+
return str.AsBool(defaultValue);
26+
}
27+
28+
/// <summary>
29+
/// Gets the specified configuration setting and converts it to a boolean.
30+
/// </summary>
31+
/// <param name="name">Key name of the setting</param>
32+
/// <param name="config">The config option to retrieve the value from</param>
33+
/// <param name="defaultValue">Value to use if the setting doesn't exist or is unable to be parsed</param>
34+
/// <returns>True if the setting exists and is set to a value that can be parsed as true, false otherwise</returns>
35+
public static bool GetConfigSettingAsBool(string name, IConfiguration config, bool defaultValue = false)
36+
{
37+
return config.GetValue(name, defaultValue.ToString()).AsBool(defaultValue);
38+
}
39+
40+
/// <summary>
41+
/// Converts the string into an equivalent boolean value. This is used instead of Convert.ToBool since that
42+
/// doesn't handle converting the string value "1".
43+
/// </summary>
44+
/// <param name="str">The string to convert</param>
45+
/// <param name="defaultValue">Value to use if the string is unable to be converted, default is false</param>
46+
/// <returns></returns>
47+
private static bool AsBool(this string str, bool defaultValue = false)
48+
{
49+
switch (str.ToLowerInvariant())
50+
{
51+
case "true":
52+
case "1":
53+
case "yes":
54+
return true;
55+
case "false":
56+
case "0":
57+
case "no":
58+
return false;
59+
default:
60+
return defaultValue;
61+
}
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)