src/WebJobs.Extensions.DurableTask/LinuxAppServiceLogger.cs (117 lines of code) (raw):
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.IO;
using System.Threading;
using Newtonsoft.Json.Linq;
namespace Microsoft.Azure.WebJobs.Extensions.DurableTask
{
/// <summary>
/// In charge of logging services for our linux App Service offerings: Consumption and Dedicated.
/// In Consumption, we log to the console and identify our log by a prefix.
/// In Dedicated, we log asynchronously to a pre-defined logging path.
/// This class is utilized by <c>EventSourceListener</c> to write logs corresponding to
/// specific EventSource providers.
/// </summary>
internal class LinuxAppServiceLogger : IDisposable
{
private const string ConsolePrefix = "MS_DURABLE_FUNCTION_EVENTS_LOGS";
#pragma warning disable SA1401 // Fields should be private
internal static string LoggingPath = "/var/log/functionsLogs/durableeventsJSON.log";
#pragma warning restore SA1401 // Fields should be private
// logging metadata
private readonly string roleInstance;
private readonly string tenant;
private readonly int procID;
private readonly string stamp;
private readonly string primaryStamp;
// if true, we write to console (linux consumption), else to a file (linux dedicated).
private readonly bool writeToConsole;
private readonly LinuxAppServiceFileLogger fileLogger;
/// <summary>
/// Create a LinuxAppServiceLogger instance.
/// </summary>
/// <param name="writeToConsole">If true, write to console (linux consumption) else to a file (dedicated).</param>
/// <param name="containerName">The app's container name.</param>
/// <param name="tenant">The app's tenant.</param>
/// <param name="stampName">The app's stamp.</param>
public LinuxAppServiceLogger(
bool writeToConsole,
string containerName,
string tenant,
string stampName)
{
// Initializing fixed logging metadata
this.writeToConsole = writeToConsole;
// Since the values below are obtained via a NameResolver, they might be null.
// Attempting to serialize a null value results in exceptions, or even worse, wrong logs,
// so we need to be careful.
if (!string.IsNullOrEmpty(containerName))
{
this.roleInstance = "App-" + containerName;
}
if (!string.IsNullOrEmpty(tenant))
{
this.tenant = tenant;
}
if (!string.IsNullOrEmpty(stampName))
{
this.stamp = stampName;
// TODO: The logic below does not apply to ASEs. We'll need to revisit this in the near future.
var finalCharIndex = stampName.Length - 1;
this.primaryStamp = char.IsLetter(stampName[finalCharIndex]) ? stampName.Remove(finalCharIndex) : stampName;
}
using (var process = Process.GetCurrentProcess())
{
this.procID = process.Id;
}
// Initialize file logger, if in Linux Dedicated
if (!writeToConsole)
{
// int tenMbInBytes = 10000000;
string fname = Path.GetFileName(LinuxAppServiceLogger.LoggingPath);
string dir = Path.GetDirectoryName(LinuxAppServiceLogger.LoggingPath);
this.fileLogger = new LinuxAppServiceFileLogger(fname, dir);
}
}
/// <summary>
/// Given EventSource message data, we generate a JSON-string that we can log.
/// </summary>
/// <param name="eventData">An EventSource message, usually generated by an EventListener.</param>
/// <param name="extensionGuid">A GUID of the extension hosting this object.</param>
/// <returns>A JSON-formatted string representing the input.</returns>
private string GenerateLogStr(EventWrittenEventArgs eventData, Guid extensionGuid)
{
var values = eventData.Payload;
var keys = eventData.PayloadNames;
// We pack them into a JSON
JObject json = new JObject
{
{ "ProviderName", eventData.EventSource.Name },
{ "TaskName", eventData.EventName },
{ "EventId", eventData.EventId },
{ "EventTimestamp", DateTime.UtcNow },
{ "Pid", this.procID },
{ "Tid", Thread.CurrentThread.ManagedThreadId },
{ "Level", (int)eventData.Level },
{ "ExtensionGUID", extensionGuid },
};
if (!string.IsNullOrEmpty(this.stamp) && !string.IsNullOrEmpty(this.primaryStamp))
{
json.Add("EventStampName", this.stamp);
json.Add("EventPrimaryStampName", this.primaryStamp);
}
if (!(this.roleInstance is null))
{
json.Add("RoleInstance", this.roleInstance);
}
if (!(this.tenant is null))
{
json.Add("Tenant", this.tenant);
}
// Add payload elements
for (int i = 0; i < values.Count; i++)
{
json.Add(keys[i], JToken.FromObject(values[i]));
}
// Add ActivityId and RelatedActivityId, if non-null
if (!eventData.ActivityId.Equals(Guid.Empty))
{
json.Add("ActivityId", eventData.ActivityId);
}
if (!eventData.RelatedActivityId.Equals(Guid.Empty))
{
json.Add("RelatedActivityId", eventData.RelatedActivityId);
}
// Generate string-representation of JSON.
// Newtonsoft should take care of removing newlines for us.
// It is also important to specify no formatting to avoid
// pretty printing.
string logString = json.ToString(Newtonsoft.Json.Formatting.None);
return logString;
}
/// <summary>
/// Log EventSource message data in Linux AppService.
/// </summary>
/// <param name="eventData">An EventSource message, usually generated by an EventListener.</param>
/// <param name="extensionGuid">A GUID identifying the extension hosting this object.</param>
public void Log(EventWrittenEventArgs eventData, Guid extensionGuid)
{
// Generate JSON string to log based on the EventSource message
string jsonString = this.GenerateLogStr(eventData, extensionGuid);
// We write to console in Linux Consumption
if (this.writeToConsole)
{
// We're ignoring exceptions in the unobserved Task
string consoleLine = ConsolePrefix + " " + jsonString;
_ = Console.Out.WriteLineAsync(consoleLine);
}
else
{
// We write to a file in Linux Dedicated
// Our file logger already handles file rolling (archiving) and deletion of old logs
this.fileLogger.Log(jsonString);
}
}
public void Dispose()
{
this.fileLogger?.Dispose();
}
}
}