Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ root = true
[*.cs]

dotnet_analyzer_diagnostic.severity = error
# 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
dotnet_diagnostic.IDE0130.severity = none

# Documentation related errors, remove once they are fixed
dotnet_diagnostic.CS1591.severity = none
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Further information on the Azure SQL binding for Azure Functions is also availab
- [Single Row](#single-row)
- [Primary Keys and Identity Columns](#primary-keys-and-identity-columns)
- [Known Issues](#known-issues)
- [Telemetry](#telemetry)
- [Trademarks](#trademarks)

## Quick Start
Expand Down Expand Up @@ -526,6 +527,10 @@ This changes if one of the primary key columns is an identity column though. In
- 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.
- 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.

## 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 `AZUREFUNCTIONS_SQLBINDINGS_TELEMETRY_OPTOUT` environment variable or the `AzureFunctionsSqlBindingsTelemetryOptOut` app setting (in your `*.settings.json` file) to '1', 'true' or 'yes';

## Trademarks

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.
2 changes: 2 additions & 0 deletions builds/azure-pipelines/template-steps-build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ steps:
env:
TEST_SERVER: '$(testServer)'
NODE_MODULES_PATH: '$(nodeModulesPath)'
AZUREFUNCTIONS_SQLBINDINGS_TELEMETRY_OPTOUT: '1'
inputs:
command: test
projects: '${{ parameters.solution }}'
Expand All @@ -201,6 +202,7 @@ steps:
displayName: '.NET Test on Linux'
env:
SA_PASSWORD: '$(serverPassword)'
AZUREFUNCTIONS_SQLBINDINGS_TELEMETRY_OPTOUT: '1'
inputs:
command: test
projects: '${{ parameters.solution }}'
Expand Down
2 changes: 1 addition & 1 deletion samples/local.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet",
"SqlConnectionString": ""
}
}
}
3 changes: 1 addition & 2 deletions src/Microsoft.Azure.WebJobs.Extensions.Sql.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/Azure/azure-functions-sql-extension</PackageProjectUrl>
<PackageIcon>pkgicon.png</PackageIcon>
<!-- Need to set root namespace to empty for IDE0130 to work properly - otherwise it errors out on top-level namespaces for some reason -->
<RootNamespace></RootNamespace>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.ApplicationInsights" Version="2.19.0" />
<PackageReference Include="Microsoft.Azure.WebJobs" Version="3.0.*" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="3.0.*" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.*" PrivateAssets="All" />
Expand Down
1 change: 1 addition & 0 deletions src/SqlBindingConfigProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public void Initialize(ExtensionConfigContext context)
{
throw new ArgumentNullException(nameof(context));
}
Telemetry.Telemetry.Instance.Initialize(this._configuration, this._loggerFactory);
#pragma warning disable CS0618 // Fine to use this for our stuff
FluentBindingRule<SqlAttribute> inputOutputRule = context.AddBindingRule<SqlAttribute>();
var converter = new SqlConverter(this._configuration);
Expand Down
157 changes: 157 additions & 0 deletions src/Telemetry/Telemetry.cs
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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are you suggesting? Because there's two things happening here :

  1. We create a clone of the commonMeasurements dictionary (we don't want to modify the original)
  2. We then add all the passed in measurements to that dictionary, overwriting any properties that might exist

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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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;
}
}
}
}

32 changes: 32 additions & 0 deletions src/Telemetry/TelemetryCommonProperties.cs
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}
};
}
}
}

21 changes: 21 additions & 0 deletions src/Telemetry/TelemetryUtils.cs
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);
}
}
}
64 changes: 64 additions & 0 deletions src/Utils.cs
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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
}
}
}
}
36 changes: 36 additions & 0 deletions test/Common/TestConfiguration.cs
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];
}
}
}
Loading