Plan:
When your start building a system that consists of more than one application you can get such problems with logging:
To solve the issues above let's try to answer the next questions:
Let's check this code example below:
var a = new TelemetryClient(new TelemetryConfiguration("the key"));
a.TrackTrace("TelemetryClient test 1",
new Dictionary<string, string>() { { "someKey", "some value" } });
Code above will create a log entry that will be displayed as following:
This data in custom dimensions will allow you to make queries without using regular expressions.
TelemetryClient doing pretty well its job but let's see pros and cons:
pros:
cons:
Let's check a code example
..., ILogger log)
{
var calloutId = 1234;
var testData = "test data";
var data = new WeatherForecast{ Date = DateTimeOffset.Now, Summary = "test", TemperatureCelsius = 15};
var jsonString = JsonConvert.SerializeObject(data);
log.LogInformation("ILogger log22 {calloutId}, {testData}, {data}, {jsonString}",
calloutId, testData, data, jsonString);
....
This code will be displayed in the Application Insights as following:
ILogger also doing pretty well bat also has some disadvantages:
pros:
cons:
.ToString()
is used for each param passed to the ILogger, so for objects passed will be displayed only their fully qualified name (requires to use .ToJson()
before passing objects, except anonymous ones)Intermediate result 1:
TelemetryClient
is a good choice for those who going to stay with AppInsights forever, on the other hand going with the default ILogger
will add prefixes to your logged data but provide flexibility for the destination of the logs.ILogger
to Application insights: To encapsulate that weird way to provide additional properties to logging templates you can create a set of extension methods. Also, you can include there additional useful data like row number, file path and method name.
So usage of prepared extensions may look like this:
..., ILogger log)
{
var calloutId = 1234;
var companyId = 1234;
log.LogInfo("Something happened", calloutId, companyId);
log.LogWarn("Something happened", calloutId, companyId);
try
{
throw new ArgumentOutOfRangeException("Test exception");
}
catch (Exception ex)
{
log.LogErr(ex, "Something happened", calloutId, companyId);
}
....
Here is a code example for extension methods used above:
public static class LoggerExtensions
{
public static void LogInfo(this ILogger logger, string reason, int calloutId, int? companyId = null, int? customerId = null,
[CallerMemberName] string method = "",
[CallerFilePath] string srcFilePath = "",
[CallerLineNumber] int srcLineNumber = 0)
{
logger.LogInformation("{reason}, {calloutId}, {companyId}, {customerId}, {method}, {srcFilePath}, {srcLineNumber}",
reason, calloutId, companyId, customerId, method, srcFilePath, srcLineNumber);
}
public static void LogWarn(this ILogger logger, string reason, int calloutId, int? companyId = null, int? customerId = null,
[CallerMemberName] string method = "",
[CallerFilePath] string srcFilePath = "",
[CallerLineNumber] int srcLineNumber = 0)
{
logger.LogWarning("{reason}, {calloutId}, {companyId}, {customerId}, {method}, {srcFilePath}, {srcLineNumber}",
reason, calloutId, companyId, customerId, method, srcFilePath, srcLineNumber);
}
public static void LogErr(this ILogger logger, Exception ex, string reason, int calloutId, int? companyId = null, int? customerId = null,
[CallerMemberName] string method = "",
[CallerFilePath] string srcFilePath = "",
[CallerLineNumber] int srcLineNumber = 0)
{
logger.LogError("{exType}, {reason}, {calloutId}, {companyId}, {customerId}, {method}, {srcFilePath}, {srcLineNumber}, {exDetails}",
ex.GetType().Name, reason, calloutId, companyId, customerId, method, srcFilePath, srcLineNumber, ex.ToString());
}
}
Intermediate result 2: With such a logging approach you can group all your logs by method name or source file as well as by any data you provide as an additional column.
custom extensions for ILogger
vs ILogger
vs LoggerMessage
Results for 1000 logs per logger candidate running on Azure Function.
iLoggerTimer: 00:00:05.0994452
enhancedLoggerTimer: 00:00:05.4228425
loggerMsgTimer: 00:00:05.1233116
I see good results for
custom extensions for ILogger
including the fact it is also logging line number, file path and method name and have the ability to log even more useful data.private readonly SpecificLogger _log;
private static Func<ILogger, int, int, IDisposable> scope = LoggerMessage.DefineScope<int, int>("Scope calloutId: {calloutId}, companyId: {companyId}");
private static readonly Action<ILogger, int, Exception> testLog = LoggerMessage.Define<int>(LogLevel.Information, new EventId(4, "TestEventName"), "loggerMsg: Something happened {customerId}");
public Function2(SpecificLogger log)
{
_log = log;
}
[FunctionName("Function2")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]
HttpRequest req,
ILogger log)
{
var calloutId = 1234;
var companyId = 2345;
var customerId = 3456;
var times = 1000;
var iLoggerTimer = Stopwatch.StartNew();
using (log.BeginScope("Scope calloutId: {calloutId}, companyId: {companyId}", calloutId, companyId))
{
for (int i = 0; i < times; i++)
{
log.LogInformation("iLogger: Something happened {customerId}", customerId);
}
}
iLoggerTimer.Stop();
var enhancedLoggerTimer = Stopwatch.StartNew();
_log.SetCalloutId(calloutId);
_log.SetCompanyId(companyId);
for (int i = 0; i < times; i++)
{
_log.LogWarn("enhancedLogger: Something happened 4", customerId: customerId);
}
enhancedLoggerTimer.Stop();
var loggerMsgTimer = Stopwatch.StartNew();
using (scope(log, calloutId, companyId))
{
for (int i = 0; i < times; i++)
{
testLog(log, customerId, null);
}
}
loggerMsgTimer.Stop();
var result =
$"iLoggerTimer: {iLoggerTimer.Elapsed}; enhancedLoggerTimer: {enhancedLoggerTimer.Elapsed}, loggerMsgTimer: {loggerMsgTimer.Elapsed}";
return new OkObjectResult(result);
}
Thanks for reading this article, hope it helps you!