Last active
July 19, 2020 23:59
-
-
Save cgillum/1d57461ba69a98ecfe6c4306793552ab to your computer and use it in GitHub Desktop.
Convert EventSource to ILogger for DurableTask.AzureStorage
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace ConsoleApp1 | |
{ | |
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics.Tracing; | |
using System.Linq; | |
using System.Reflection; | |
using DurableTask.AzureStorage; | |
using Microsoft.CodeAnalysis; | |
using Microsoft.CodeAnalysis.CSharp; | |
using Microsoft.CodeAnalysis.CSharp.Formatting; | |
using Microsoft.CodeAnalysis.CSharp.Syntax; | |
using Microsoft.CodeAnalysis.Formatting; | |
using Microsoft.Extensions.Logging; | |
using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory; | |
static class CodeGen | |
{ | |
// Given an existing (currently hardcoded) EventSource class, generates a bunch of ILogger infrastructure code | |
public static void Generate() | |
{ | |
// Resources for learning how to use Roslyn to generate code: | |
// https://carlos.mendible.com/2017/03/02/create-a-class-with-net-core-and-roslyn/ (seems to be the most up-to-date) | |
// http://roslynquoter.azurewebsites.net/ (use with caution - this creates excessively verbose Roslyn code - most .WithXXX() methods could be simplified to .AddXXX()) | |
var workspace = new AdhocWorkspace(); | |
var cu = SF.CompilationUnit().AddMembers( | |
CreateEventIdsClass(), | |
CreateLogEventsClass(), | |
CreateLogHelperClass()); | |
var options = workspace.Options; | |
workspace.Options.WithChangedOption(CSharpFormattingOptions.IndentBraces, true); | |
SyntaxNode formattedNode = Formatter.Format(cu, workspace, options); | |
formattedNode.WriteTo(Console.Out); | |
} | |
static NamespaceDeclarationSyntax CreateEventIdsClass() | |
{ | |
var ns = SF.NamespaceDeclaration(SF.IdentifierName("DurableTask.AzureStorage.Logging")); | |
var eventIdsClass = SF.ClassDeclaration("EventIds").AddModifiers(SF.Token(SyntaxKind.StaticKeyword)); | |
var consts = new List<MemberDeclarationSyntax>(); | |
foreach ((MethodInfo method, EventAttribute @event) in GetTraceEventMethods()) | |
{ | |
consts.Add(CreateConst("int", method.Name, @event.EventId.ToString())); | |
} | |
return ns.AddMembers(eventIdsClass.AddMembers(consts.ToArray()).NormalizeWhitespace()); | |
} | |
static NamespaceDeclarationSyntax CreateLogEventsClass() | |
{ | |
var ns = SF.NamespaceDeclaration(SF.IdentifierName("DurableTask.AzureStorage.Logging")).AddUsings( | |
SF.UsingDirective(SF.IdentifierName("System")), | |
SF.UsingDirective(SF.IdentifierName("DurableTask.Core.Logging")), | |
SF.UsingDirective(SF.IdentifierName("Microsoft.Extensions.Logging"))); | |
var logEventsClass = SF.ClassDeclaration("LogEvents").AddModifiers(SF.Token(SyntaxKind.StaticKeyword)); | |
foreach ((MethodInfo method, EventAttribute @event) in GetTraceEventMethods()) | |
{ | |
// Class, base class, interface, and constructor | |
var eventClass = SF.ClassDeclaration(method.Name) | |
.AddModifiers(SF.Token(SyntaxKind.InternalKeyword)) | |
.AddBaseListTypes( | |
SF.SimpleBaseType(SF.IdentifierName("StructuredLogEvent")), | |
SF.SimpleBaseType(SF.IdentifierName("IEventSourceEvent"))); | |
var constructorParams = new List<ParameterSyntax>(); | |
var propertyDeclarations = new List<PropertyDeclarationSyntax>(); | |
var propertyAssignmentStatements = new List<StatementSyntax>(); | |
// Structured log properties | |
foreach (ParameterInfo param in method.GetParameters()) | |
{ | |
if (param.Name == "ExtensionVersion") | |
{ | |
// skip ExtensionVersion - that comes from a static variable | |
continue; | |
} | |
string paramName = GetParamName(param); | |
string paramType = GetTypeName(param.ParameterType); | |
constructorParams.Add( | |
SF.Parameter(SF.Identifier(paramName)).WithType(SF.IdentifierName(paramType)).WithLeadingNewline()); | |
string propertyName = GetPropertyName(param); | |
var property = SF.PropertyDeclaration(SF.IdentifierName(paramType), propertyName) | |
.AddModifiers(SF.Token(SyntaxKind.PublicKeyword)) | |
.AddAccessorListAccessors(SF.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithSemicolonToken(SF.Token(SyntaxKind.SemicolonToken))) | |
.WithTrailingTrivia(SF.CarriageReturnLineFeed, SF.CarriageReturnLineFeed); | |
if (param.Name != "relatedActivityId") | |
{ | |
// Don't include relatedActivityId as a serialized property | |
property = property.AddAttributeLists(SF.AttributeList(SF.SingletonSeparatedList(SF.Attribute(SF.IdentifierName("StructuredLogField"))))); | |
} | |
propertyDeclarations.Add(property); | |
propertyAssignmentStatements.Add( | |
SF.ParseStatement($"this.{propertyName} = {paramName};").WithTrailingTrivia(SF.CarriageReturnLineFeed)); | |
} | |
// Add the constructor | |
eventClass = eventClass.AddMembers( | |
SF.ConstructorDeclaration(method.Name) | |
.AddModifiers(SF.Token(SyntaxKind.PublicKeyword)) | |
.AddParameterListParameters(constructorParams.ToArray()).WithLeadingTrivia(SF.CarriageReturnLineFeed) | |
.AddBodyStatements(propertyAssignmentStatements.ToArray())); | |
// Add the log event properties | |
eventClass = eventClass.AddMembers(propertyDeclarations.ToArray()); | |
// Add the EventId property expression | |
eventClass = eventClass.AddMembers( | |
SF.PropertyDeclaration(SF.IdentifierName("EventId"), "EventId") | |
.AddModifiers(SF.Token(SyntaxKind.PublicKeyword), SF.Token(SyntaxKind.OverrideKeyword)) | |
.WithExpressionBody(SF.ArrowExpressionClause( | |
SF.ObjectCreationExpression(SF.IdentifierName("EventId")).AddArgumentListArguments( | |
SF.Argument(SF.ParseExpression($"EventIds.{method.Name}")).WithLeadingNewline(), | |
SF.Argument(SF.ParseExpression($"nameof(EventIds.{method.Name})")).WithLeadingNewline()))) | |
.WithSemicolonToken(SF.Token(SyntaxKind.SemicolonToken))); | |
// Add the LogLevel property expression | |
eventClass = eventClass.AddMembers( | |
SF.PropertyDeclaration(SF.IdentifierName("LogLevel"), "Level") | |
.AddModifiers(SF.Token(SyntaxKind.PublicKeyword), SF.Token(SyntaxKind.OverrideKeyword)) | |
.WithExpressionBody(SF.ArrowExpressionClause(SF.ParseExpression($"LogLevel.{GetLogLevel(@event)}"))) | |
.WithSemicolonToken(SF.Token(SyntaxKind.SemicolonToken))); | |
// Add the GetLogMessage method expression | |
eventClass = eventClass.AddMembers( | |
SF.MethodDeclaration(SF.IdentifierName("string"), "GetLogMessage") | |
.AddModifiers(SF.Token(SyntaxKind.PublicKeyword), SF.Token(SyntaxKind.OverrideKeyword)) | |
.WithExpressionBody(SF.ArrowExpressionClause(SF.ParseExpression(@$"$""TODO: Add formatted message here""").WithLeadingNewline())) | |
.WithSemicolonToken(SF.Token(SyntaxKind.SemicolonToken)) | |
.WithTrailingTrivia(SF.CarriageReturnLineFeed, SF.CarriageReturnLineFeed)); | |
// Add the WriteEventSource method expression | |
var eventSourceWriteExpression = SF.InvocationExpression(SF.ParseExpression($"AnalyticsEventSource.Log.{method.Name}")) | |
.AddArgumentListArguments( | |
method.GetParameters() | |
.Select(p => p.Name == "ExtensionVersion" ? "Utils.ExtensionVersion" : p.Name) | |
.Select(name => SF.Argument(SF.ParseExpression(name == "Utils.ExtensionVersion" ? name : $"this.{name}").WithLeadingNewline())) | |
.ToArray()); | |
eventClass = eventClass.AddMembers( | |
SF.MethodDeclaration(SF.IdentifierName("void"), "WriteEventSource") | |
.WithExplicitInterfaceSpecifier(SF.ExplicitInterfaceSpecifier(SF.IdentifierName("IEventSourceEvent"))) | |
.WithExpressionBody(SF.ArrowExpressionClause(eventSourceWriteExpression)) | |
.WithSemicolonToken(SF.Token(SyntaxKind.SemicolonToken))); | |
logEventsClass = logEventsClass.AddMembers(eventClass); | |
} | |
return ns.AddMembers(logEventsClass); | |
} | |
static NamespaceDeclarationSyntax CreateLogHelperClass() | |
{ | |
var ns = SF.NamespaceDeclaration(SF.IdentifierName("DurableTask.AzureStorage.Logging")).AddUsings( | |
SF.UsingDirective(SF.IdentifierName("System")), | |
SF.UsingDirective(SF.IdentifierName("DurableTask.Core.Logging")), | |
SF.UsingDirective(SF.IdentifierName("Microsoft.Extensions.Logging"))); | |
var logHelperClass = SF.ClassDeclaration("LogHelper") | |
.AddMembers( | |
SF.FieldDeclaration(SF.VariableDeclaration(SF.IdentifierName("ILogger")).AddVariables(SF.VariableDeclarator("log"))) | |
.AddModifiers(SF.Token(SyntaxKind.ReadOnlyKeyword)), | |
SF.ConstructorDeclaration("LogHelper") | |
.AddModifiers(SF.Token(SyntaxKind.PublicKeyword)) | |
.AddParameterListParameters( | |
SF.Parameter(SF.Identifier("log")).WithType(SF.IdentifierName("ILogger"))) | |
.WithBody(SF.Block( | |
SF.ParseStatement("this.log = log;")))); | |
foreach ((MethodInfo method, EventAttribute @event) in GetTraceEventMethods()) | |
{ | |
MethodDeclarationSyntax methodDeclaration = | |
SF.MethodDeclaration(SF.IdentifierName("void"), method.Name).AddModifiers(SF.Token(SyntaxKind.InternalKeyword)); | |
var parameters = new List<ParameterSyntax>(); | |
var arguments = new List<ArgumentSyntax>(); | |
foreach (ParameterInfo param in method.GetParameters()) | |
{ | |
if (param.Name == "ExtensionVersion") | |
{ | |
// skip ExtensionVersion - that comes from a static variable | |
continue; | |
} | |
string paramName = GetParamName(param); | |
string paramType = GetTypeName(param.ParameterType); | |
parameters.Add(SF.Parameter(SF.Identifier(paramName)).WithType(SF.IdentifierName(paramType)).WithLeadingNewline()); | |
arguments.Add(SF.Argument(SF.ParseExpression(paramName)).WithLeadingNewline()); | |
} | |
methodDeclaration = methodDeclaration.AddParameterListParameters( | |
parameters.ToArray()).WithLeadingTrivia(SF.CarriageReturnLineFeed); | |
var variable = SF.VariableDeclaration(SF.IdentifierName("var")) | |
.AddVariables(SF.VariableDeclarator("logEvent").WithInitializer(SF.EqualsValueClause( | |
SF.ObjectCreationExpression(SF.QualifiedName(SF.IdentifierName("LogEvents"), SF.IdentifierName(method.Name))) | |
.AddArgumentListArguments(arguments.ToArray()).WithLeadingTrivia(SF.CarriageReturnLineFeed)))); | |
methodDeclaration = methodDeclaration.AddBodyStatements( | |
SF.LocalDeclarationStatement(variable), | |
SF.ParseStatement("this.WriteStructuredLog(logEvent);")); | |
logHelperClass = logHelperClass.AddMembers(methodDeclaration); | |
} | |
logHelperClass = logHelperClass.AddMembers( | |
SF.MethodDeclaration(SF.IdentifierName("void"), "WriteStructuredLog") | |
.AddBodyStatements( | |
SF.ParseStatement("LoggingExtensions.LogDurableEvent(this.log, logEvent, exception);")) | |
.AddParameterListParameters( | |
SF.Parameter(SF.Identifier("logEvent")).WithType(SF.IdentifierName("ILogEvent")), | |
SF.Parameter(SF.Identifier("exception")) | |
.WithType(SF.IdentifierName("Exception")) | |
.WithDefault(SF.EqualsValueClause(SF.LiteralExpression(SyntaxKind.NullLiteralExpression))))); | |
return ns.AddMembers(logHelperClass); | |
} | |
static string GetParamName(ParameterInfo param) | |
{ | |
string name = param.Name; | |
if (char.IsUpper(name[0])) | |
{ | |
// convert "InstanceId" to "instanceId" | |
name = char.ToLower(name[0]) + name.Substring(1); | |
} | |
return name; | |
} | |
static string GetPropertyName(ParameterInfo param) | |
{ | |
string name = param.Name; | |
if (char.IsLower(name[0])) | |
{ | |
// convert "InstanceId" to "instanceId" | |
name = char.ToUpper(name[0]) + name.Substring(1); | |
} | |
return name; | |
} | |
static string GetLogLevel(EventAttribute @event) | |
{ | |
switch (@event.Level) | |
{ | |
case EventLevel.Informational: | |
return nameof(LogLevel.Information); | |
case EventLevel.Verbose: | |
return nameof(LogLevel.Debug); | |
case EventLevel.Error: | |
return nameof(LogLevel.Error); | |
case EventLevel.Warning: | |
return nameof(LogLevel.Warning); | |
case EventLevel.Critical: | |
return nameof(LogLevel.Critical); | |
default: | |
throw new NotSupportedException($"{@event.Level} is not supported!"); | |
} | |
} | |
static IEnumerable<(MethodInfo, EventAttribute)> GetTraceEventMethods() | |
{ | |
// Reflect over the EventSource methods to get the trace event names, IDs, and parameters | |
Type eventSourceType = typeof(AzureStorageOrchestrationService).Assembly.GetType( | |
"DurableTask.AzureStorage.AnalyticsEventSource", | |
throwOnError: true); | |
foreach (MethodInfo method in eventSourceType.GetMethods()) | |
{ | |
EventAttribute @event = method.GetCustomAttribute<EventAttribute>(); | |
if (@event != null) | |
{ | |
yield return (method, @event); | |
} | |
} | |
} | |
static PropertyDeclarationSyntax CreateConst(string type, string name, string expression) | |
{ | |
PropertyDeclarationSyntax property = SF.PropertyDeclaration(SF.IdentifierName(type), name); | |
return property | |
.AddModifiers(SF.Token(SyntaxKind.PublicKeyword), SF.Token(SyntaxKind.ConstKeyword)) | |
.WithInitializer(SF.EqualsValueClause(SF.ParseExpression(expression))) | |
.WithSemicolonToken(SF.Token(SyntaxKind.SemicolonToken)); | |
} | |
static TSyntax WithLeadingNewline<TSyntax>(this TSyntax syntax) where TSyntax : SyntaxNode | |
{ | |
return syntax.WithLeadingTrivia(SF.CarriageReturnLineFeed, SF.Whitespace(" ")); | |
} | |
static string GetTypeName(Type type) | |
{ | |
// Prefer C# keywords over runtime type names | |
if (type == typeof(string)) | |
{ | |
return "string"; | |
} | |
else if (type == typeof(int)) | |
{ | |
return "int"; | |
} | |
else if (type == typeof(long)) | |
{ | |
return "long"; | |
} | |
else if (type == typeof(bool)) | |
{ | |
return "bool"; | |
} | |
else if (type == typeof(double)) | |
{ | |
return "double"; | |
} | |
else | |
{ | |
return type.Name; | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment