Skip to content

Instantly share code, notes, and snippets.

@cgillum
Last active July 19, 2020 23:59
Show Gist options
  • Save cgillum/1d57461ba69a98ecfe6c4306793552ab to your computer and use it in GitHub Desktop.
Save cgillum/1d57461ba69a98ecfe6c4306793552ab to your computer and use it in GitHub Desktop.
Convert EventSource to ILogger for DurableTask.AzureStorage
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