-
Notifications
You must be signed in to change notification settings - Fork 63
Add AppInsights #196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add AppInsights #196
Changes from all commits
0538944
521edcb
11a4139
642ef66
daf0519
eea3e5f
ed59955
d83a1a8
8b74eca
72e5a02
77872a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. See License.txt in the project root for license information. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics; | ||
| using System.Runtime.InteropServices; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.ApplicationInsights; | ||
| using Microsoft.ApplicationInsights.Extensibility; | ||
| using Microsoft.Extensions.Configuration; | ||
| using Microsoft.Azure.WebJobs.Logging; | ||
|
|
||
| namespace Microsoft.Azure.WebJobs.Extensions.Sql.Telemetry | ||
| { | ||
| public sealed class Telemetry | ||
| { | ||
| internal static Telemetry Instance = new Telemetry(); | ||
|
|
||
| private const string EventsNamespace = "azure-functions-sql-bindings"; | ||
| internal static string CurrentSessionId; | ||
| private TelemetryClient _client; | ||
| private Dictionary<string, string> _commonProperties; | ||
| private Dictionary<string, double> _commonMeasurements; | ||
| private Task _trackEventTask; | ||
| private ILogger _logger; | ||
| private bool _initialized; | ||
| private const string InstrumentationKey = "98697a1c-1416-486a-99ac-c6c74ebe5ebd"; | ||
| /// <summary> | ||
| /// The environment variable used for opting out of telemetry | ||
| /// </summary> | ||
| public const string TelemetryOptoutEnvVar = "AZUREFUNCTIONS_SQLBINDINGS_TELEMETRY_OPTOUT"; | ||
| /// <summary> | ||
| /// The app setting used for opting out of telemetry | ||
| /// </summary> | ||
| public const string TelemetryOptoutSetting = "AzureFunctionsSqlBindingsTelemetryOptOut"; | ||
|
|
||
| public const string WelcomeMessage = @"Azure SQL binding for Azure Functions | ||
| --------------------- | ||
| Telemetry | ||
| --------- | ||
| 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'; | ||
| "; | ||
|
|
||
| public void Initialize(IConfiguration config, ILoggerFactory loggerFactory) | ||
| { | ||
| this._logger = loggerFactory.CreateLogger(LogCategories.Bindings); | ||
| this.Enabled = !(Utils.GetEnvironmentVariableAsBool(TelemetryOptoutEnvVar) || Utils.GetConfigSettingAsBool(TelemetryOptoutSetting, config)); | ||
| if (!this.Enabled) | ||
| { | ||
| this._logger.LogInformation("Telemetry disabled"); | ||
| return; | ||
| } | ||
| this._logger.LogInformation(WelcomeMessage); | ||
| // Store the session ID in a static field so that it can be reused | ||
| CurrentSessionId = Guid.NewGuid().ToString(); | ||
|
|
||
| string productVersion = typeof(Telemetry).Assembly.GetName().Version.ToString(); | ||
| //initialize in task to offload to parallel thread | ||
| this._trackEventTask = Task.Factory.StartNew(() => this.InitializeTelemetry(productVersion)); | ||
| this._initialized = true; | ||
| } | ||
|
|
||
| public bool Enabled { get; private set; } | ||
|
|
||
| public void TrackEvent(string eventName, IDictionary<string, string> properties, | ||
| IDictionary<string, double> measurements) | ||
| { | ||
| if (!this._initialized || !this.Enabled) | ||
| { | ||
| return; | ||
| } | ||
| this._logger.LogInformation($"Sending event {eventName}"); | ||
|
|
||
| //continue task in existing parallel thread | ||
| this._trackEventTask = this._trackEventTask.ContinueWith( | ||
| x => this.TrackEventTask(eventName, properties, measurements) | ||
| ); | ||
| } | ||
|
|
||
| private void InitializeTelemetry(string productVersion) | ||
| { | ||
| try | ||
| { | ||
| var config = new TelemetryConfiguration(InstrumentationKey); | ||
| this._client = new TelemetryClient(config); | ||
| this._client.Context.Session.Id = CurrentSessionId; | ||
| this._client.Context.Device.OperatingSystem = RuntimeInformation.OSDescription; | ||
|
|
||
| this._commonProperties = new TelemetryCommonProperties(productVersion).GetTelemetryCommonProperties(); | ||
| this._commonMeasurements = new Dictionary<string, double>(); | ||
| } | ||
| catch (Exception e) | ||
| { | ||
| this._client = null; | ||
| // we don't want to fail the tool if telemetry fails. | ||
| Debug.Fail(e.ToString()); | ||
| } | ||
| } | ||
|
|
||
| private void TrackEventTask( | ||
| string eventName, | ||
| IDictionary<string, string> properties, | ||
| IDictionary<string, double> measurements) | ||
| { | ||
| if (this._client is null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| Dictionary<string, string> eventProperties = this.GetEventProperties(properties); | ||
| Dictionary<string, double> eventMeasurements = this.GetEventMeasures(measurements); | ||
|
|
||
| this._client.TrackEvent($"{EventsNamespace}/{eventName}", eventProperties, eventMeasurements); | ||
| this._client.Flush(); | ||
| } | ||
| catch (Exception e) | ||
| { | ||
| Debug.Fail(e.ToString()); | ||
| } | ||
| } | ||
|
|
||
| private Dictionary<string, double> GetEventMeasures(IDictionary<string, double> measurements) | ||
| { | ||
| var eventMeasurements = new Dictionary<string, double>(this._commonMeasurements); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we do any of these other solutions instead (to clean up this code)? https://stackoverflow.com/questions/6422091/convert-idictionary-to-dictionary
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are you suggesting? Because there's two things happening here :
We want to create a clone of the dictionary regardless (using the dictionary passed in could be problematic, we can't trust the caller to be doing the right thing there). And as far as I know there isn't a way to create a "combined" dictionary from two separate sources. Unless you're just saying that the logic is fine you just think we could compress it - in which case maybe? I'd have to play around with it, but that doesn't really seem worth the effort here given how short they are already.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I missed line 128 where we're creating a clone of commonMeasurements. I thought this was just cloning in quite a roundabout fashion. And yeah was mainly around compressing this into fewer lines. It's fine. |
||
| if (measurements != null) | ||
| { | ||
| foreach (KeyValuePair<string, double> measurement in measurements) | ||
| { | ||
| eventMeasurements[measurement.Key] = measurement.Value; | ||
| } | ||
| } | ||
| return eventMeasurements; | ||
| } | ||
|
|
||
| private Dictionary<string, string> GetEventProperties(IDictionary<string, string> properties) | ||
| { | ||
| if (properties != null) | ||
| { | ||
| var eventProperties = new Dictionary<string, string>(this._commonProperties); | ||
| foreach (KeyValuePair<string, string> property in properties) | ||
| { | ||
| eventProperties[property.Key] = property.Value; | ||
| } | ||
| return eventProperties; | ||
| } | ||
| else | ||
| { | ||
| return this._commonProperties; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. See License.txt in the project root for license information. | ||
|
|
||
| using System.Collections.Generic; | ||
| using System.Runtime.InteropServices; | ||
|
|
||
| namespace Microsoft.Azure.WebJobs.Extensions.Sql.Telemetry | ||
| { | ||
| public class TelemetryCommonProperties | ||
| { | ||
| private readonly string _productVersion; | ||
|
|
||
| public TelemetryCommonProperties( | ||
| string productVersion) | ||
| { | ||
| this._productVersion = productVersion; | ||
| } | ||
|
|
||
| private const string OSVersion = "OSVersion"; | ||
| private const string ProductVersion = "ProductVersion"; | ||
|
|
||
| public Dictionary<string, string> GetTelemetryCommonProperties() | ||
| { | ||
| return new Dictionary<string, string> | ||
| { | ||
| {OSVersion, RuntimeInformation.OSDescription}, | ||
| {ProductVersion, this._productVersion} | ||
| }; | ||
| } | ||
| } | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. See License.txt in the project root for license information. | ||
|
|
||
| using System.Collections.Generic; | ||
| using Microsoft.Data.SqlClient; | ||
|
|
||
| namespace Microsoft.Azure.WebJobs.Extensions.Sql.Telemetry | ||
| { | ||
| public static class TelemetryUtils | ||
| { | ||
| /// <summary> | ||
| /// Adds common connection properties to the property bag for a telemetry event. | ||
| /// </summary> | ||
| /// <param name="props">The property bag to add our connection properties to</param> | ||
| /// <param name="conn">The connection to add properties of</param> | ||
| public static void AddConnectionProps(this Dictionary<string, string> props, SqlConnection conn) | ||
| { | ||
| props.Add(nameof(SqlConnection.ServerVersion), conn.ServerVersion); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. See License.txt in the project root for license information. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ideally, we'd have some tests for this file 😄 |
||
|
|
||
| using System; | ||
| using Microsoft.Extensions.Configuration; | ||
|
|
||
| namespace Microsoft.Azure.WebJobs.Extensions.Sql | ||
| { | ||
| public static class Utils | ||
| { | ||
| /// <summary> | ||
| /// Gets the specified environment variable and converts it to a boolean. | ||
| /// </summary> | ||
| /// <param name="name">Name of the environment variable</param> | ||
| /// <param name="defaultValue">Value to use if the variable doesn't exist or is unable to be parsed</param> | ||
| /// <returns>True if the variable exists and is set to a value that can be parsed as true, false otherwise</returns> | ||
| public static bool GetEnvironmentVariableAsBool(string name, bool defaultValue = false) | ||
| { | ||
| string str = Environment.GetEnvironmentVariable(name); | ||
| if (string.IsNullOrEmpty(str)) | ||
| { | ||
| return defaultValue; | ||
| } | ||
|
|
||
| return str.AsBool(defaultValue); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the specified configuration setting and converts it to a boolean. | ||
| /// </summary> | ||
| /// <param name="name">Key name of the setting</param> | ||
| /// <param name="config">The config option to retrieve the value from</param> | ||
| /// <param name="defaultValue">Value to use if the setting doesn't exist or is unable to be parsed</param> | ||
| /// <returns>True if the setting exists and is set to a value that can be parsed as true, false otherwise</returns> | ||
| public static bool GetConfigSettingAsBool(string name, IConfiguration config, bool defaultValue = false) | ||
| { | ||
| return config.GetValue(name, defaultValue.ToString()).AsBool(defaultValue); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Converts the string into an equivalent boolean value. This is used instead of Convert.ToBool since that | ||
| /// doesn't handle converting the string value "1". | ||
| /// </summary> | ||
| /// <param name="str">The string to convert</param> | ||
| /// <param name="defaultValue">Value to use if the string is unable to be converted, default is false</param> | ||
| /// <returns></returns> | ||
| private static bool AsBool(this string str, bool defaultValue = false) | ||
| { | ||
| switch (str.ToLowerInvariant()) | ||
| { | ||
| case "true": | ||
| case "1": | ||
| case "yes": | ||
| return true; | ||
| case "false": | ||
| case "0": | ||
| case "no": | ||
| return false; | ||
| default: | ||
| return defaultValue; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. See License.txt in the project root for license information. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using Microsoft.Extensions.Configuration; | ||
| using Microsoft.Extensions.Primitives; | ||
|
|
||
| namespace Microsoft.Azure.WebJobs.Extensions.Sql.Tests.Common | ||
| { | ||
| internal class TestConfiguration : IConfiguration | ||
| { | ||
| private readonly IDictionary<string, IConfigurationSection> _sections = new Dictionary<string, IConfigurationSection>(); | ||
| public void AddSection(string key, IConfigurationSection section) | ||
| { | ||
| this._sections[key] = section; | ||
| } | ||
|
|
||
| string IConfiguration.this[string key] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } | ||
|
|
||
| IEnumerable<IConfigurationSection> IConfiguration.GetChildren() | ||
| { | ||
| throw new NotImplementedException(); | ||
| } | ||
|
|
||
| IChangeToken IConfiguration.GetReloadToken() | ||
| { | ||
| throw new NotImplementedException(); | ||
| } | ||
|
|
||
| IConfigurationSection IConfiguration.GetSection(string key) | ||
| { | ||
| return this._sections[key]; | ||
| } | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.