v3 Upgrade Guide
Previous versions of Tool Telemetry relied on some features which are now deprecated. This document outlines the changes and how to adapt your code accordingly. Please be aware we are relying on the “classic” AppInsights SDK, and .NET is now recommending an OpenTelemetry-based integration. These changes to the telemetry APIs should help prepare for such a migration in the future.
Besides an easier upgrade path to OpenTelemetry, other improvements include integration with built-in .NET telemetry and 3rd party libraries, configurable log levels at runtime, multiple log sinks/targets configuration, and source generator support for type-safe and consistent telemetry.
Telemetry Configuration and Setup
Where previously you would configure the library using the global Telemetry object:
Telemetry.Configure(appInsightsConnectionString: "...");Now it recommended to use dependency injection:
var services = new ServiceCollection() .AddSQTechToolTelemetry(appInsightsConnectionString: "...") .BuildServiceProvider();The AddSQTechToolTelemetry extension method is shorthand for the following configuration:
services .AddLogging(builder => builder.AddApplicationInsights( configureTelemetryConfiguration: (config) => config.ConnectionString = "...", configureApplicationInsightsLoggerOptions: _ => { })) .AddFilter("System.Net.Http.HttpClient", LogLevel.Warning)) .Configure<TelemetryConfiguration>(config => Array.ForEach(ToolTelemetry.DefaultInitializers, config.TelemetryInitializers.Add))HTTP telemetry is filtered to only warnings/errors since it can be rather noisy.
Previously if you didn’t provide a connection string, it would default to a SQTech resource, but now you must provide one explicitly:
var services = new ServiceCollection() .AddSQTechToolTelemetry(appInsightsConnectionString: ToolTelemetry.DefaultAppInsightsConnectionString) .BuildServiceProvider();Personally Identifiable Information (PII)
Please note that ToolTelemetry.DefaultInitializers includes one piece of personal data: the username of the tool user.
You can disable PII collection via a configuration option: AddSQTechToolTelemetry(disablePII: true)
Logger Use and Injection
The easiest way to obtain an ILogger instance is by using dependency injection.
For example, if you previously wrote the following code:
public class MyClass { public void Run() { Telemetry.TrackEvent.MyCustomEvent(namedProp1: "lalala", namedProp2: 456); }}
new MyClass().Run();Then upgrading this class to use an injected ILogger might look like so:
public class MyClass(ILogger<MyClass> logger) { public void Run() { logger.LogInformation("My custom event with {NamedProp1} and {NamedProp2}", "lalala", 456); }}
var services = new ServiceCollection() .AddSQTechToolTelemetry(appInsightsConnectionString: "...") .AddSingleton<MyClass>() .BuildServiceProvider();services.GetRequiredService<MyClass>().Run();Alternatively, for classes that are difficult to convert to dependency injection, you can use ILoggerFactory and/or a global ILogger instance:
public static class MyLog { public static ILogger Log { get; set; }}
public class MyClass { public void Run() { MyLog.Log.LogInformation("My custom event with {NamedProp1} and {NamedProp2}", "lalala", 456); }}MyLog.Log = services.GetRequiredService<ILoggerFactory>().CreateLogger(string.Empty);new MyClass().Run();Differences in Telemetry Output
The “customEvents” table is replaced by “traces” and “exceptions”, but the data remains in an equivalent format apart from a few minor changes:
- Previously, boolean properties such as
IsDebugBuildwere logged to AppInsights in lowercase “true”/“false”, where now they’re “True”/“False”. - Previously, the
user_Idproperty was just the username, and now it includes the domain such as “REDMOND\user”. - Previously, the
application_Versionproperty was determined automatically based on the calling assembly, and now it’s not.- You can collect data on the calling method automatically using default parameters on source generated logger methods.
Metrics
Metrics are supported by default, but some additional setup is required. It’s recommended to start by reading .NET’s documentation.
You’ll define your “instruments” in a group such as “SQTech.MyTool” and then configure AppInsights to collect them like so:
var services = new ServiceCollection() .AddSQTechToolTelemetry(appInsightsConnectionString: "...") .Configure<AppInsightsMetricListenerOptions>(i => i.Metrics = ["SQTech.MyTool"]) .BuildServiceProvider();services.GetRequiredService<AppInsightsMetricListener>().Start();For example, if you previously wrote the following code:
Telemetry.TrackMetric.MyMetric(value: 2, instance: "label1-foo");It would now be written like so, using IMeterFactory and a Counter instance instead of the static Telemetry object:
var meter = services.GetRequiredService<IMeterFactory>().Create("SQTech.MyTool");Counter<int> myMetric = meter.CreateCounter<int>("MyMetric");myMetric.Add(2, tag1: KeyValuePair.Create("Label", (object?)1), tag2: KeyValuePair.Create("Tag", (object?)"foo"));If your metrics are in a library not using dependency injection, you can also construct meters manually:
var meter = new Meter("SQTech.MyTool");Please note that the metric group name is not collected, so you might want to prefix your metric names like “SQTech.MyTool.MyMetric”. See Microsoft’s best practices for metric naming as well as OpenTelemetry’s guidelines.
Source Generated Methods
Where the previous APIs worked via reflection and dynamic member names (allowing great flexibility), it’s now recommended to use source-generated logging methods.
Traces and Events
For example, if you previously wrote the following code:
Telemetry.TrackInfo.TraceWithNamedProperty(namedProp1: "lalala", namedProp2: 456);It would now be written like so, using an ILogger instance instead of the static Telemetry object:
logger.TraceWithNamedProperty(namedProp1: "lalala", namedProp2: 456);
public static partial class Log{ [LoggerMessage(Level = LogLevel.Information, Message = "Trace with named property")] public static partial void TraceWithNamedProperty(this ILogger logger, string namedProp1, int namedProp2);}Exceptions
Error logs can be easily upgraded to use ILogger APIs. For example, if you previously wrote:
Telemetry.TrackException.ApplicationCrashed(ex);It would now be written like so, using an ILogger instance instead of the static Telemetry object:
logger.LogError(ex, "Application crashed");Metrics
Similar source-generated APIs are also available for tracking metrics. For example, if you previously wrote the following code:
Telemetry.TrackMetric.MyMetric(value: 2, instance: "label1-foo");It would now be written like so, using IMeterFactory and a MyMetric instance instead of the static Telemetry object:
var meter = services.GetRequiredService<IMeterFactory>().Create("SQTech.MyTool");MyMetric myMetric = MyMetrics.CreateMyMetric(meter);myMetric.Add(2, new MetricTags { Label = "1", Tag = "foo" });
public struct MetricTags{ public string Label { get; set; } public string Tag { get; set; }}public static partial class MyMetrics{ [Counter<int>(typeof(MetricTags))] public static partial MyMetric CreateMyMetric(Meter meter);}Default Parameters
The source-generated APIs also support default parameters, which can be used to automatically collect data on the calling method:
[LoggerMessage( Level = LogLevel.Information, Message = "[{callerFilePath}:{callerLineNumber} {callerMemberName}] Trace with {namedProp1} and {namedProp2}")]public static partial void TraceWithNamedProperty( this ILogger logger, string namedProp1, int namedProp2, [CallerMemberName] string? callerMemberName = default, [CallerFilePath] string? callerFilePath = default, [CallerLineNumber] int? callerLineNumber = default, string assemblyVersion = ThisAssembly.AssemblyVersion);Telemetry Context
Where you used to be able to (or required) to pass a context object to each telemetry call, you can now use either default parameters or telemetry initializers. For example, if you used to set “moduleName” via the context, it could now look like so:
[LoggerMessage(Level = LogLevel.Information, Message = "Trace with {namedProp1} and {namedProp2}")]public static partial void TraceWithNamedProperty( this ILogger logger, string namedProp1, int namedProp2, string moduleName = "MyModule");Flushing
Although there isn’t a global telemetry object anymore, it is still recommended to call Flush() and wait when shutting down to allow time for telemetry collection to finish.
This can be accomplished by creating a ITelemetryChannel object and passing it to the AddSQTechToolTelemetry method:
var telemetryChannel = new InMemoryChannel();var services = new ServiceCollection() .AddSQTechToolTelemetry(appInsightsConnectionString: "...", telemetryChannel: telemetryChannel)...telemetryChannel.Flush();Thread.Sleep(TimeSpan.FromSeconds(1));