diff --git a/GitReleaseManager.yaml b/GitReleaseManager.yaml index 17a777e..152b6c0 100644 --- a/GitReleaseManager.yaml +++ b/GitReleaseManager.yaml @@ -1,5 +1,5 @@ create: - include-footer: true + include-footer: false footer-heading: Where to get it footer-content: You can download this release from [nuget.org](https://www.nuget.org/packages/CakeMail.RestClient/{milestone}) footer-includes-milestone: true diff --git a/README.md b/README.md index 5e1e25f..50e4c9e 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,9 @@ Pre-release packages are available on my MyGet feed: - `IF`, `ELSEIF`, `ELSE` and `ENDIF` must be upper case which means that ``[IF `myfield` = "myValue"]`` is valid but ``[if `myfield` = "myValue"]`` is not. - square bracket is the delimeter which means that ``[IF `myfield` = "myValue"]`` is valid but ``{IF `myfield` = "myValue"}`` is not. - the name of the data field must be surrounded by back ticks (not to be confused with single quotes) which means that ``[IF `firstname` = "Bob"]`` is valid but ``[IF 'firstname' = "Bob"]`` is not. - - you can only compare a field to a constant value which means that ``[IF `firstname` = "Bob"]`` is valid but ``[IF `firstname` = `nickname`]`` is not. + - you can only compare a field to a constant value and you can't compare a field to another field which means that ``[IF `firstname` = "Bob"]`` is valid but ``[IF `firstname` = `nickname`]`` is not. - the constant value must be surrounded with double quotes when it's a string which means that ``[IF `firstname` = "Bob"]`` is valid but ``[IF `firstname` = 'Bob']`` is not. - - the constant value must not be surrounded by any quotes is a numeric valu when it's a numeric value which means that ``[IF `age` >= 18]`` is valid. + - the constant value must not be surrounded by any quotes when it's a numeric value which means that ``[IF `age` >= 18]`` is valid. - the data field must be on the left side of the comparison which means that ``[IF `gender` = \"Male\"]`` is valid but ``[IF \"Male\" = `gender`]`` is not. - you can have multiple conditions seperated by `AND` or `OR` which means that ``IF `firstname` = "Bob" AND `lastname` = "Smith"]`` is valid. - the acceptable operators when comparing a field to a string value: `<`, `<=`, `=`, `!=`, `>=`, `>`, `LIKE` and `NOT LIKE` diff --git a/Source/CakeMail.RestClient.IntegrationTests/ConsoleLogProvider.cs b/Source/CakeMail.RestClient.IntegrationTests/ConsoleLogProvider.cs new file mode 100644 index 0000000..aec468a --- /dev/null +++ b/Source/CakeMail.RestClient.IntegrationTests/ConsoleLogProvider.cs @@ -0,0 +1,49 @@ +namespace CakeMail.RestClient.IntegrationTests +{ + using Logging; + using System; + using System.Globalization; + + // Inspired by: https://github.com/damianh/LibLog/blob/master/src/LibLog.Example.ColoredConsoleLogProvider/ColoredConsoleLogProvider.cs + public class ConsoleLogProvider : ILogProvider + { + + public Logger GetLogger(string name) + { + return (logLevel, messageFunc, exception, formatParameters) => + { + if (messageFunc == null) + { + return true; // All log levels are enabled + } + + var message = string.Format(CultureInfo.InvariantCulture, messageFunc(), formatParameters); + if (exception != null) + { + message = message + "|" + exception; + } + Console.WriteLine("{0} | {1} | {2} | {3}", DateTime.UtcNow, logLevel, name, message); + + return true; + }; + } + + public IDisposable OpenNestedContext(string message) + { + return NullDisposable.Instance; + } + + public IDisposable OpenMappedContext(string key, string value) + { + return NullDisposable.Instance; + } + + private class NullDisposable : IDisposable + { + internal static readonly IDisposable Instance = new NullDisposable(); + + public void Dispose() + { } + } + } +} diff --git a/Source/CakeMail.RestClient.IntegrationTests/Program.cs b/Source/CakeMail.RestClient.IntegrationTests/Program.cs index 821142b..24c2a6b 100644 --- a/Source/CakeMail.RestClient.IntegrationTests/Program.cs +++ b/Source/CakeMail.RestClient.IntegrationTests/Program.cs @@ -1,4 +1,5 @@ -using System; +using CakeMail.RestClient.Logging; +using System; namespace CakeMail.RestClient.IntegrationTests { @@ -6,17 +7,55 @@ public class Program { public static void Main() { - Console.WriteLine("{0} Executing all CakeMail API methods ... {0}", new string('=', 10)); + // ----------------------------------------------------------------------------- + + // Do you want to proxy requests through Fiddler (useful for debugging)? + var useFiddler = false; + + // As an alternative to Fiddler, you can display debug information about + // every HTTP request/response in the console. This is useful for debugging + // purposes but the amount of information can be overwhelming. + var debugHttpMessagesToConsole = true; + // ----------------------------------------------------------------------------- + + var proxy = useFiddler ? new WebProxy("http://localhost:8888") : null; + var apiKey = Environment.GetEnvironmentVariable("CAKEMAIL_APIKEY"); + var userName = Environment.GetEnvironmentVariable("CAKEMAIL_USERNAME"); + var password = Environment.GetEnvironmentVariable("CAKEMAIL_PASSWORD"); + var overrideClientId = Environment.GetEnvironmentVariable("CAKEMAIL_OVERRIDECLIENTID"); + + if (debugHttpMessagesToConsole) + { + LogProvider.SetCurrentLogProvider(new ConsoleLogProvider()); + } try { - ExecuteAllMethods(); + var client = new CakeMailRestClient(apiKey, proxy); + var loginInfo = client.Users.LoginAsync(userName, password).Result; + var clientId = string.IsNullOrEmpty(overrideClientId) ? loginInfo.ClientId : long.Parse(overrideClientId); + var userKey = loginInfo.UserKey; + + TimezonesTests.ExecuteAllMethods(client).Wait(); + CountriesTests.ExecuteAllMethods(client).Wait(); + ClientsTests.ExecuteAllMethods(client, userKey, clientId).Wait(); + UsersTests.ExecuteAllMethods(client, userKey, clientId).Wait(); + PermissionsTests.ExecuteAllMethods(client, userKey, clientId).Wait(); + CampaignsTests.ExecuteAllMethods(client, userKey, clientId).Wait(); + ListsTests.ExecuteAllMethods(client, userKey, clientId).Wait(); + TemplatesTests.ExecuteAllMethods(client, userKey, clientId).Wait(); + SuppressionListsTests.ExecuteAllMethods(client, userKey, clientId).Wait(); + RelaysTests.ExecuteAllMethods(client, userKey, clientId).Wait(); + TriggersTests.ExecuteAllMethods(client, userKey, clientId).Wait(); + MailingsTests.ExecuteAllMethods(client, userKey, clientId).Wait(); } catch (Exception e) { - Console.WriteLine(""); - Console.WriteLine(""); - Console.WriteLine("An error has occured: {0}", (e.InnerException ?? e).Message); + Console.WriteLine("\n\n**************************************************"); + Console.WriteLine("**************************************************"); + Console.WriteLine($"AN EXCEPTION OCCURED: {(e.InnerException ?? e).Message}"); + Console.WriteLine("**************************************************"); + Console.WriteLine("**************************************************"); } finally { @@ -25,46 +64,11 @@ public static void Main() { Console.ReadKey(); } - - Console.WriteLine(""); - Console.WriteLine("Press any key..."); + Console.WriteLine("\n\n*************************"); + Console.WriteLine("All tests completed"); + Console.WriteLine("Press any key to exit"); Console.ReadKey(); } } - - private static void ExecuteAllMethods() - { - // ----------------------------------------------------------------------------- - - // Do you want to proxy requests through Fiddler (useful for debugging)? - var useFiddler = false; - - // ----------------------------------------------------------------------------- - - - var proxy = useFiddler ? new WebProxy("http://localhost:8888") : null; - var apiKey = Environment.GetEnvironmentVariable("CAKEMAIL_APIKEY"); - var userName = Environment.GetEnvironmentVariable("CAKEMAIL_USERNAME"); - var password = Environment.GetEnvironmentVariable("CAKEMAIL_PASSWORD"); - var overrideClientId = Environment.GetEnvironmentVariable("CAKEMAIL_OVERRIDECLIENTID"); - - var api = new CakeMailRestClient(apiKey, proxy); - var loginInfo = api.Users.LoginAsync(userName, password).Result; - var clientId = string.IsNullOrEmpty(overrideClientId) ? loginInfo.ClientId : long.Parse(overrideClientId); - var userKey = loginInfo.UserKey; - - TimezonesTests.ExecuteAllMethods(api).Wait(); - CountriesTests.ExecuteAllMethods(api).Wait(); - ClientsTests.ExecuteAllMethods(api, userKey, clientId).Wait(); - UsersTests.ExecuteAllMethods(api, userKey, clientId).Wait(); - PermissionsTests.ExecuteAllMethods(api, userKey, clientId).Wait(); - CampaignsTests.ExecuteAllMethods(api, userKey, clientId).Wait(); - ListsTests.ExecuteAllMethods(api, userKey, clientId).Wait(); - TemplatesTests.ExecuteAllMethods(api, userKey, clientId).Wait(); - SuppressionListsTests.ExecuteAllMethods(api, userKey, clientId).Wait(); - RelaysTests.ExecuteAllMethods(api, userKey, clientId).Wait(); - TriggersTests.ExecuteAllMethods(api, userKey, clientId).Wait(); - MailingsTests.ExecuteAllMethods(api, userKey, clientId).Wait(); - } } } diff --git a/Source/CakeMail.RestClient/CakeMailRestClient.cs b/Source/CakeMail.RestClient/CakeMailRestClient.cs index 93a7698..74b9a9b 100644 --- a/Source/CakeMail.RestClient/CakeMailRestClient.cs +++ b/Source/CakeMail.RestClient/CakeMailRestClient.cs @@ -161,6 +161,7 @@ public CakeMailRestClient(string apiKey, string host = DEFAULT_HOST, HttpClient _fluentClient.BaseClient.DefaultRequestHeaders.Add("apikey", this.ApiKey); _fluentClient.Filters.Remove(); + _fluentClient.Filters.Add(new DiagnosticHandler()); _fluentClient.Filters.Add(new CakeMailErrorHandler()); Campaigns = new Campaigns(_fluentClient); diff --git a/Source/CakeMail.RestClient/Logging/LibLog.cs b/Source/CakeMail.RestClient/Logging/LibLog.cs new file mode 100644 index 0000000..39e972f --- /dev/null +++ b/Source/CakeMail.RestClient/Logging/LibLog.cs @@ -0,0 +1,2329 @@ +//------------------------------------------------------------------------------ +// +// This tag ensures the content of this file is not analyzed by StyleCop.Analyzers +// +//------------------------------------------------------------------------------ + +//=============================================================================== +// LibLog +// +// https://github.com/damianh/LibLog +//=============================================================================== +// Copyright © 2011-2015 Damian Hickey. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +//=============================================================================== + +// ReSharper disable PossibleNullReferenceException + +// Define LIBLOG_PORTABLE conditional compilation symbol for PCL compatibility +// +// Define LIBLOG_PUBLIC to enable ability to GET a logger (LogProvider.For<>() etc) from outside this library. NOTE: +// this can have unintended consequences of consumers of your library using your library to resolve a logger. If the +// reason is because you want to open this functionality to other projects within your solution, +// consider [InternalsVisibleTo] instead. +// +// Define LIBLOG_PROVIDERS_ONLY if your library provides its own logging API and you just want to use the +// LibLog providers internally to provide built in support for popular logging frameworks. + +#pragma warning disable 1591 + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "CakeMail.RestClient.Logging")] +[assembly: SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Scope = "member", Target = "CakeMail.RestClient.Logging.Logger.#Invoke(CakeMail.RestClient.Logging.LogLevel,System.Func`1,System.Exception,System.Object[])")] + +// If you copied this file manually, you need to change all "CakeMail.RestClient.Logging" so not to clash with other libraries +// that use LibLog +#if LIBLOG_PROVIDERS_ONLY +namespace CakeMail.RestClient.Logging.LibLog +#else +namespace CakeMail.RestClient.Logging +#endif +{ + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; +#if LIBLOG_PROVIDERS_ONLY + using CakeMail.RestClient.Logging.LibLog.LogProviders; +#else + using CakeMail.RestClient.Logging.LogProviders; +#endif + using System; +#if !LIBLOG_PROVIDERS_ONLY + using System.Diagnostics; +#if !LIBLOG_PORTABLE + using System.Runtime.CompilerServices; +#endif +#endif + +#if LIBLOG_PROVIDERS_ONLY + internal +#else + public +#endif + delegate bool Logger(LogLevel logLevel, Func messageFunc, Exception exception = null, params object[] formatParameters); + +#if !LIBLOG_PROVIDERS_ONLY + /// + /// Simple interface that represent a logger. + /// +#if LIBLOG_PUBLIC + public +#else + internal +#endif + interface ILog + { + /// + /// Log a message the specified log level. + /// + /// The log level. + /// The message function. + /// An optional exception. + /// Optional format parameters for the message generated by the messagefunc. + /// true if the message was logged. Otherwise false. + /// + /// Note to implementers: the message func should not be called if the loglevel is not enabled + /// so as not to incur performance penalties. + /// + /// To check IsEnabled call Log with only LogLevel and check the return value, no event will be written. + /// + bool Log(LogLevel logLevel, Func messageFunc, Exception exception = null, params object[] formatParameters); + } +#endif + + /// + /// The log level. + /// +#if LIBLOG_PROVIDERS_ONLY + internal +#else + public +#endif + enum LogLevel + { + Trace, + Debug, + Info, + Warn, + Error, + Fatal + } + +#if !LIBLOG_PROVIDERS_ONLY +#if LIBLOG_PUBLIC + public +#else + internal +#endif + static partial class LogExtensions + { + public static bool IsDebugEnabled(this ILog logger) + { + GuardAgainstNullLogger(logger); + return logger.Log(LogLevel.Debug, null); + } + + public static bool IsErrorEnabled(this ILog logger) + { + GuardAgainstNullLogger(logger); + return logger.Log(LogLevel.Error, null); + } + + public static bool IsFatalEnabled(this ILog logger) + { + GuardAgainstNullLogger(logger); + return logger.Log(LogLevel.Fatal, null); + } + + public static bool IsInfoEnabled(this ILog logger) + { + GuardAgainstNullLogger(logger); + return logger.Log(LogLevel.Info, null); + } + + public static bool IsTraceEnabled(this ILog logger) + { + GuardAgainstNullLogger(logger); + return logger.Log(LogLevel.Trace, null); + } + + public static bool IsWarnEnabled(this ILog logger) + { + GuardAgainstNullLogger(logger); + return logger.Log(LogLevel.Warn, null); + } + + public static void Debug(this ILog logger, Func messageFunc) + { + GuardAgainstNullLogger(logger); + logger.Log(LogLevel.Debug, messageFunc); + } + + public static void Debug(this ILog logger, string message) + { + if (logger.IsDebugEnabled()) + { + logger.Log(LogLevel.Debug, message.AsFunc()); + } + } + + public static void Debug(this ILog logger, string message, params object[] args) + { + logger.DebugFormat(message, args); + } + + public static void Debug(this ILog logger, Exception exception, string message, params object[] args) + { + logger.DebugException(message, exception, args); + } + + public static void DebugFormat(this ILog logger, string message, params object[] args) + { + if (logger.IsDebugEnabled()) + { + logger.LogFormat(LogLevel.Debug, message, args); + } + } + + public static void DebugException(this ILog logger, string message, Exception exception) + { + if (logger.IsDebugEnabled()) + { + logger.Log(LogLevel.Debug, message.AsFunc(), exception); + } + } + + public static void DebugException(this ILog logger, string message, Exception exception, params object[] formatParams) + { + if (logger.IsDebugEnabled()) + { + logger.Log(LogLevel.Debug, message.AsFunc(), exception, formatParams); + } + } + + public static void Error(this ILog logger, Func messageFunc) + { + GuardAgainstNullLogger(logger); + logger.Log(LogLevel.Error, messageFunc); + } + + public static void Error(this ILog logger, string message) + { + if (logger.IsErrorEnabled()) + { + logger.Log(LogLevel.Error, message.AsFunc()); + } + } + + public static void Error(this ILog logger, string message, params object[] args) + { + logger.ErrorFormat(message, args); + } + + public static void Error(this ILog logger, Exception exception, string message, params object[] args) + { + logger.ErrorException(message, exception, args); + } + + public static void ErrorFormat(this ILog logger, string message, params object[] args) + { + if (logger.IsErrorEnabled()) + { + logger.LogFormat(LogLevel.Error, message, args); + } + } + + public static void ErrorException(this ILog logger, string message, Exception exception, params object[] formatParams) + { + if (logger.IsErrorEnabled()) + { + logger.Log(LogLevel.Error, message.AsFunc(), exception, formatParams); + } + } + + public static void Fatal(this ILog logger, Func messageFunc) + { + logger.Log(LogLevel.Fatal, messageFunc); + } + + public static void Fatal(this ILog logger, string message) + { + if (logger.IsFatalEnabled()) + { + logger.Log(LogLevel.Fatal, message.AsFunc()); + } + } + + public static void Fatal(this ILog logger, string message, params object[] args) + { + logger.FatalFormat(message, args); + } + + public static void Fatal(this ILog logger, Exception exception, string message, params object[] args) + { + logger.FatalException(message, exception, args); + } + + public static void FatalFormat(this ILog logger, string message, params object[] args) + { + if (logger.IsFatalEnabled()) + { + logger.LogFormat(LogLevel.Fatal, message, args); + } + } + + public static void FatalException(this ILog logger, string message, Exception exception, params object[] formatParams) + { + if (logger.IsFatalEnabled()) + { + logger.Log(LogLevel.Fatal, message.AsFunc(), exception, formatParams); + } + } + + public static void Info(this ILog logger, Func messageFunc) + { + GuardAgainstNullLogger(logger); + logger.Log(LogLevel.Info, messageFunc); + } + + public static void Info(this ILog logger, string message) + { + if (logger.IsInfoEnabled()) + { + logger.Log(LogLevel.Info, message.AsFunc()); + } + } + + public static void Info(this ILog logger, string message, params object[] args) + { + logger.InfoFormat(message, args); + } + + public static void Info(this ILog logger, Exception exception, string message, params object[] args) + { + logger.InfoException(message, exception, args); + } + + public static void InfoFormat(this ILog logger, string message, params object[] args) + { + if (logger.IsInfoEnabled()) + { + logger.LogFormat(LogLevel.Info, message, args); + } + } + + public static void InfoException(this ILog logger, string message, Exception exception, params object[] formatParams) + { + if (logger.IsInfoEnabled()) + { + logger.Log(LogLevel.Info, message.AsFunc(), exception, formatParams); + } + } + + public static void Trace(this ILog logger, Func messageFunc) + { + GuardAgainstNullLogger(logger); + logger.Log(LogLevel.Trace, messageFunc); + } + + public static void Trace(this ILog logger, string message) + { + if (logger.IsTraceEnabled()) + { + logger.Log(LogLevel.Trace, message.AsFunc()); + } + } + + public static void Trace(this ILog logger, string message, params object[] args) + { + logger.TraceFormat(message, args); + } + + public static void Trace(this ILog logger, Exception exception, string message, params object[] args) + { + logger.TraceException(message, exception, args); + } + + public static void TraceFormat(this ILog logger, string message, params object[] args) + { + if (logger.IsTraceEnabled()) + { + logger.LogFormat(LogLevel.Trace, message, args); + } + } + + public static void TraceException(this ILog logger, string message, Exception exception, params object[] formatParams) + { + if (logger.IsTraceEnabled()) + { + logger.Log(LogLevel.Trace, message.AsFunc(), exception, formatParams); + } + } + + public static void Warn(this ILog logger, Func messageFunc) + { + GuardAgainstNullLogger(logger); + logger.Log(LogLevel.Warn, messageFunc); + } + + public static void Warn(this ILog logger, string message) + { + if (logger.IsWarnEnabled()) + { + logger.Log(LogLevel.Warn, message.AsFunc()); + } + } + + public static void Warn(this ILog logger, string message, params object[] args) + { + logger.WarnFormat(message, args); + } + + public static void Warn(this ILog logger, Exception exception, string message, params object[] args) + { + logger.WarnException(message, exception, args); + } + + public static void WarnFormat(this ILog logger, string message, params object[] args) + { + if (logger.IsWarnEnabled()) + { + logger.LogFormat(LogLevel.Warn, message, args); + } + } + + public static void WarnException(this ILog logger, string message, Exception exception, params object[] formatParams) + { + if (logger.IsWarnEnabled()) + { + logger.Log(LogLevel.Warn, message.AsFunc(), exception, formatParams); + } + } + + // ReSharper disable once UnusedParameter.Local + private static void GuardAgainstNullLogger(ILog logger) + { + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + } + + private static void LogFormat(this ILog logger, LogLevel logLevel, string message, params object[] args) + { + logger.Log(logLevel, message.AsFunc(), null, args); + } + + // Avoid the closure allocation, see https://gist.github.com/AArnott/d285feef75c18f6ecd2b + private static Func AsFunc(this T value) where T : class + { + return value.Return; + } + + private static T Return(this T value) + { + return value; + } + } +#endif + + /// + /// Represents a way to get a + /// +#if LIBLOG_PROVIDERS_ONLY + internal +#else + public +#endif + interface ILogProvider + { + /// + /// Gets the specified named logger. + /// + /// Name of the logger. + /// The logger reference. + Logger GetLogger(string name); + + /// + /// Opens a nested diagnostics context. Not supported in EntLib logging. + /// + /// The message to add to the diagnostics context. + /// A disposable that when disposed removes the message from the context. + IDisposable OpenNestedContext(string message); + + /// + /// Opens a mapped diagnostics context. Not supported in EntLib logging. + /// + /// A key. + /// A value. + /// A disposable that when disposed removes the map from the context. + IDisposable OpenMappedContext(string key, string value); + } + + /// + /// Provides a mechanism to create instances of objects. + /// +#if LIBLOG_PROVIDERS_ONLY + internal +#else + public +#endif + static class LogProvider + { +#if !LIBLOG_PROVIDERS_ONLY + private const string NullLogProvider = "Current Log Provider is not set. Call SetCurrentLogProvider " + + "with a non-null value first."; + private static dynamic s_currentLogProvider; + private static Action s_onCurrentLogProviderSet; + + [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")] + static LogProvider() + { + IsDisabled = false; + } + + /// + /// Sets the current log provider. + /// + /// The log provider. + public static void SetCurrentLogProvider(ILogProvider logProvider) + { + s_currentLogProvider = logProvider; + + RaiseOnCurrentLogProviderSet(); + } + + /// + /// Gets or sets a value indicating whether this is logging is disabled. + /// + /// + /// true if logging is disabled; otherwise, false. + /// + public static bool IsDisabled { get; set; } + + /// + /// Sets an action that is invoked when a consumer of your library has called SetCurrentLogProvider. It is + /// important that hook into this if you are using child libraries (especially ilmerged ones) that are using + /// LibLog (or other logging abstraction) so you adapt and delegate to them. + /// + /// + internal static Action OnCurrentLogProviderSet + { + set + { + s_onCurrentLogProviderSet = value; + RaiseOnCurrentLogProviderSet(); + } + } + + internal static ILogProvider CurrentLogProvider + { + get + { + return s_currentLogProvider; + } + } + + /// + /// Gets a logger for the specified type. + /// + /// The type whose name will be used for the logger. + /// An instance of +#if LIBLOG_PUBLIC + public +#else + internal +#endif + static ILog For() + { + return GetLogger(typeof(T)); + } + +#if !LIBLOG_PORTABLE + /// + /// Gets a logger for the current class. + /// + /// An instance of + [MethodImpl(MethodImplOptions.NoInlining)] +#if LIBLOG_PUBLIC + public +#else + internal +#endif + static ILog GetCurrentClassLogger() + { + var stackFrame = new StackFrame(1, false); + return GetLogger(stackFrame.GetMethod().DeclaringType); + } +#endif + + /// + /// Gets a logger for the specified type. + /// + /// The type whose name will be used for the logger. + /// If the type is null then this name will be used as the log name instead + /// An instance of +#if LIBLOG_PUBLIC + public +#else + internal +#endif + static ILog GetLogger(Type type, string fallbackTypeName = "System.Object") + { + // If the type passed in is null then fallback to the type name specified + return GetLogger(type != null ? type.FullName : fallbackTypeName); + } + + /// + /// Gets a logger with the specified name. + /// + /// The name. + /// An instance of +#if LIBLOG_PUBLIC + public +#else + internal +#endif + static ILog GetLogger(string name) + { + ILogProvider logProvider = CurrentLogProvider ?? ResolveLogProvider(); + return logProvider == null + ? NoOpLogger.Instance + : (ILog)new LoggerExecutionWrapper(logProvider.GetLogger(name), () => IsDisabled); + } + + /// + /// Opens a nested diagnostics context. + /// + /// A message. + /// An that closes context when disposed. + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "SetCurrentLogProvider")] +#if LIBLOG_PUBLIC + public +#else + internal +#endif + static IDisposable OpenNestedContext(string message) + { + ILogProvider logProvider = CurrentLogProvider ?? ResolveLogProvider(); + + return logProvider == null + ? new DisposableAction(() => { }) + : logProvider.OpenNestedContext(message); + } + + /// + /// Opens a mapped diagnostics context. + /// + /// A key. + /// A value. + /// An that closes context when disposed. + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "SetCurrentLogProvider")] +#if LIBLOG_PUBLIC + public +#else + internal +#endif + static IDisposable OpenMappedContext(string key, string value) + { + ILogProvider logProvider = CurrentLogProvider ?? ResolveLogProvider(); + + return logProvider == null + ? new DisposableAction(() => { }) + : logProvider.OpenMappedContext(key, value); + } +#endif + +#if LIBLOG_PROVIDERS_ONLY + private +#else + internal +#endif + delegate bool IsLoggerAvailable(); + +#if LIBLOG_PROVIDERS_ONLY + private +#else + internal +#endif + delegate ILogProvider CreateLogProvider(); + +#if LIBLOG_PROVIDERS_ONLY + private +#else + internal +#endif + static readonly List> LogProviderResolvers = + new List> + { + new Tuple(SerilogLogProvider.IsLoggerAvailable, () => new SerilogLogProvider()), + new Tuple(NLogLogProvider.IsLoggerAvailable, () => new NLogLogProvider()), + new Tuple(Log4NetLogProvider.IsLoggerAvailable, () => new Log4NetLogProvider()), + new Tuple(EntLibLogProvider.IsLoggerAvailable, () => new EntLibLogProvider()), + new Tuple(LoupeLogProvider.IsLoggerAvailable, () => new LoupeLogProvider()), + }; + +#if !LIBLOG_PROVIDERS_ONLY + private static void RaiseOnCurrentLogProviderSet() + { + if (s_onCurrentLogProviderSet != null) + { + s_onCurrentLogProviderSet(s_currentLogProvider); + } + } +#endif + + [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Console.WriteLine(System.String,System.Object,System.Object)")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + internal static ILogProvider ResolveLogProvider() + { + try + { + foreach (var providerResolver in LogProviderResolvers) + { + if (providerResolver.Item1()) + { + return providerResolver.Item2(); + } + } + } + catch (Exception ex) + { +#if LIBLOG_PORTABLE + Debug.WriteLine( +#else + Console.WriteLine( +#endif + "Exception occurred resolving a log provider. Logging for this assembly {0} is disabled. {1}", + typeof(LogProvider).GetAssemblyPortable().FullName, + ex); + } + return null; + } + +#if !LIBLOG_PROVIDERS_ONLY + internal class NoOpLogger : ILog + { + internal static readonly NoOpLogger Instance = new NoOpLogger(); + + public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) + { + return false; + } + } +#endif + } + +#if !LIBLOG_PROVIDERS_ONLY + internal class LoggerExecutionWrapper : ILog + { + private readonly Logger _logger; + private readonly Func _getIsDisabled; + internal const string FailedToGenerateLogMessage = "Failed to generate log message"; + + internal LoggerExecutionWrapper(Logger logger, Func getIsDisabled = null) + { + _logger = logger; + _getIsDisabled = getIsDisabled ?? (() => false); + } + + internal Logger WrappedLogger + { + get { return _logger; } + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public bool Log(LogLevel logLevel, Func messageFunc, Exception exception = null, params object[] formatParameters) + { + if (_getIsDisabled()) + { + return false; + } + if (messageFunc == null) + { + return _logger(logLevel, null); + } + + Func wrappedMessageFunc = () => + { + try + { + return messageFunc(); + } + catch (Exception ex) + { + Log(LogLevel.Error, () => FailedToGenerateLogMessage, ex); + } + return null; + }; + return _logger(logLevel, wrappedMessageFunc, exception, formatParameters); + } + } +#endif +} + +#if LIBLOG_PROVIDERS_ONLY +namespace CakeMail.RestClient.Logging.LibLog.LogProviders +#else +namespace CakeMail.RestClient.Logging.LogProviders +#endif +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; +#if !LIBLOG_PORTABLE + using System.Diagnostics; +#endif + using System.Globalization; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; +#if !LIBLOG_PORTABLE + using System.Text; +#endif + using System.Text.RegularExpressions; + + internal abstract class LogProviderBase : ILogProvider + { + protected delegate IDisposable OpenNdc(string message); + protected delegate IDisposable OpenMdc(string key, string value); + + private readonly Lazy _lazyOpenNdcMethod; + private readonly Lazy _lazyOpenMdcMethod; + private static readonly IDisposable NoopDisposableInstance = new DisposableAction(); + + protected LogProviderBase() + { + _lazyOpenNdcMethod + = new Lazy(GetOpenNdcMethod); + _lazyOpenMdcMethod + = new Lazy(GetOpenMdcMethod); + } + + public abstract Logger GetLogger(string name); + + public IDisposable OpenNestedContext(string message) + { + return _lazyOpenNdcMethod.Value(message); + } + + public IDisposable OpenMappedContext(string key, string value) + { + return _lazyOpenMdcMethod.Value(key, value); + } + + protected virtual OpenNdc GetOpenNdcMethod() + { + return _ => NoopDisposableInstance; + } + + protected virtual OpenMdc GetOpenMdcMethod() + { + return (_, __) => NoopDisposableInstance; + } + } + + internal class NLogLogProvider : LogProviderBase + { + private readonly Func _getLoggerByNameDelegate; + private static bool s_providerIsAvailableOverride = true; + + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "LogManager")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "NLog")] + public NLogLogProvider() + { + if (!IsLoggerAvailable()) + { + throw new InvalidOperationException("NLog.LogManager not found"); + } + _getLoggerByNameDelegate = GetGetLoggerMethodCall(); + } + + public static bool ProviderIsAvailableOverride + { + get { return s_providerIsAvailableOverride; } + set { s_providerIsAvailableOverride = value; } + } + + public override Logger GetLogger(string name) + { + return new NLogLogger(_getLoggerByNameDelegate(name)).Log; + } + + public static bool IsLoggerAvailable() + { + return ProviderIsAvailableOverride && GetLogManagerType() != null; + } + + protected override OpenNdc GetOpenNdcMethod() + { + Type ndcContextType = Type.GetType("NLog.NestedDiagnosticsContext, NLog"); + MethodInfo pushMethod = ndcContextType.GetMethodPortable("Push", typeof(string)); + ParameterExpression messageParam = Expression.Parameter(typeof(string), "message"); + MethodCallExpression pushMethodCall = Expression.Call(null, pushMethod, messageParam); + return Expression.Lambda(pushMethodCall, messageParam).Compile(); + } + + protected override OpenMdc GetOpenMdcMethod() + { + Type mdcContextType = Type.GetType("NLog.MappedDiagnosticsContext, NLog"); + + MethodInfo setMethod = mdcContextType.GetMethodPortable("Set", typeof(string), typeof(string)); + MethodInfo removeMethod = mdcContextType.GetMethodPortable("Remove", typeof(string)); + ParameterExpression keyParam = Expression.Parameter(typeof(string), "key"); + ParameterExpression valueParam = Expression.Parameter(typeof(string), "value"); + + MethodCallExpression setMethodCall = Expression.Call(null, setMethod, keyParam, valueParam); + MethodCallExpression removeMethodCall = Expression.Call(null, removeMethod, keyParam); + + Action set = Expression + .Lambda>(setMethodCall, keyParam, valueParam) + .Compile(); + Action remove = Expression + .Lambda>(removeMethodCall, keyParam) + .Compile(); + + return (key, value) => + { + set(key, value); + return new DisposableAction(() => remove(key)); + }; + } + + private static Type GetLogManagerType() + { + return Type.GetType("NLog.LogManager, NLog"); + } + + private static Func GetGetLoggerMethodCall() + { + Type logManagerType = GetLogManagerType(); + MethodInfo method = logManagerType.GetMethodPortable("GetLogger", typeof(string)); + ParameterExpression nameParam = Expression.Parameter(typeof(string), "name"); + MethodCallExpression methodCall = Expression.Call(null, method, nameParam); + return Expression.Lambda>(methodCall, nameParam).Compile(); + } + + internal class NLogLogger + { + private readonly dynamic _logger; + + private static Func _logEventInfoFact; + + private static readonly object _levelTrace; + private static readonly object _levelDebug; + private static readonly object _levelInfo; + private static readonly object _levelWarn; + private static readonly object _levelError; + private static readonly object _levelFatal; + + static NLogLogger() + { + try + { + var logEventLevelType = Type.GetType("NLog.LogLevel, NLog"); + if (logEventLevelType == null) + { + throw new InvalidOperationException("Type NLog.LogLevel was not found."); + } + + var levelFields = logEventLevelType.GetFieldsPortable().ToList(); + _levelTrace = levelFields.First(x => x.Name == "Trace").GetValue(null); + _levelDebug = levelFields.First(x => x.Name == "Debug").GetValue(null); + _levelInfo = levelFields.First(x => x.Name == "Info").GetValue(null); + _levelWarn = levelFields.First(x => x.Name == "Warn").GetValue(null); + _levelError = levelFields.First(x => x.Name == "Error").GetValue(null); + _levelFatal = levelFields.First(x => x.Name == "Fatal").GetValue(null); + + var logEventInfoType = Type.GetType("NLog.LogEventInfo, NLog"); + if (logEventInfoType == null) + { + throw new InvalidOperationException("Type NLog.LogEventInfo was not found."); + } + MethodInfo createLogEventInfoMethodInfo = logEventInfoType.GetMethodPortable("Create", + logEventLevelType, typeof(string), typeof(Exception), typeof(IFormatProvider), typeof(string), typeof(object[])); + ParameterExpression loggerNameParam = Expression.Parameter(typeof(string)); + ParameterExpression levelParam = Expression.Parameter(typeof(object)); + ParameterExpression messageParam = Expression.Parameter(typeof(string)); + ParameterExpression exceptionParam = Expression.Parameter(typeof(Exception)); + UnaryExpression levelCast = Expression.Convert(levelParam, logEventLevelType); + MethodCallExpression createLogEventInfoMethodCall = Expression.Call(null, + createLogEventInfoMethodInfo, + levelCast, loggerNameParam, exceptionParam, + Expression.Constant(null, typeof(IFormatProvider)), messageParam, Expression.Constant(null, typeof(object[]))); + _logEventInfoFact = Expression.Lambda>(createLogEventInfoMethodCall, + loggerNameParam, levelParam, messageParam, exceptionParam).Compile(); + } + catch { } + } + + internal NLogLogger(dynamic logger) + { + _logger = logger; + } + + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] + public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) + { + if (messageFunc == null) + { + return IsLogLevelEnable(logLevel); + } + messageFunc = LogMessageFormatter.SimulateStructuredLogging(messageFunc, formatParameters); + + if (_logEventInfoFact != null) + { + if (IsLogLevelEnable(logLevel)) + { + var nlogLevel = this.TranslateLevel(logLevel); + Type s_callerStackBoundaryType; +#if !LIBLOG_PORTABLE + StackTrace stack = new StackTrace(); + Type thisType = GetType(); + Type knownType0 = typeof(LoggerExecutionWrapper); + Type knownType1 = typeof(LogExtensions); + //Maybe inline, so we may can't found any LibLog classes in stack + s_callerStackBoundaryType = null; + for (var i = 0; i < stack.FrameCount; i++) + { + var declaringType = stack.GetFrame(i).GetMethod().DeclaringType; + if (!IsInTypeHierarchy(thisType, declaringType) && + !IsInTypeHierarchy(knownType0, declaringType) && + !IsInTypeHierarchy(knownType1, declaringType)) + { + if (i > 1) + s_callerStackBoundaryType = stack.GetFrame(i - 1).GetMethod().DeclaringType; + break; + } + } +#else + s_callerStackBoundaryType = null; +#endif + if (s_callerStackBoundaryType != null) + _logger.Log(s_callerStackBoundaryType, _logEventInfoFact(_logger.Name, nlogLevel, messageFunc(), exception)); + else + _logger.Log(_logEventInfoFact(_logger.Name, nlogLevel, messageFunc(), exception)); + return true; + } + return false; + } + + if (exception != null) + { + return LogException(logLevel, messageFunc, exception); + } + switch (logLevel) + { + case LogLevel.Debug: + if (_logger.IsDebugEnabled) + { + _logger.Debug(messageFunc()); + return true; + } + break; + case LogLevel.Info: + if (_logger.IsInfoEnabled) + { + _logger.Info(messageFunc()); + return true; + } + break; + case LogLevel.Warn: + if (_logger.IsWarnEnabled) + { + _logger.Warn(messageFunc()); + return true; + } + break; + case LogLevel.Error: + if (_logger.IsErrorEnabled) + { + _logger.Error(messageFunc()); + return true; + } + break; + case LogLevel.Fatal: + if (_logger.IsFatalEnabled) + { + _logger.Fatal(messageFunc()); + return true; + } + break; + default: + if (_logger.IsTraceEnabled) + { + _logger.Trace(messageFunc()); + return true; + } + break; + } + return false; + } + + private static bool IsInTypeHierarchy(Type currentType, Type checkType) + { + while (currentType != null && currentType != typeof(object)) + { + if (currentType == checkType) + { + return true; + } + currentType = currentType.GetBaseTypePortable(); + } + return false; + } + + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")] + private bool LogException(LogLevel logLevel, Func messageFunc, Exception exception) + { + switch (logLevel) + { + case LogLevel.Debug: + if (_logger.IsDebugEnabled) + { + _logger.DebugException(messageFunc(), exception); + return true; + } + break; + case LogLevel.Info: + if (_logger.IsInfoEnabled) + { + _logger.InfoException(messageFunc(), exception); + return true; + } + break; + case LogLevel.Warn: + if (_logger.IsWarnEnabled) + { + _logger.WarnException(messageFunc(), exception); + return true; + } + break; + case LogLevel.Error: + if (_logger.IsErrorEnabled) + { + _logger.ErrorException(messageFunc(), exception); + return true; + } + break; + case LogLevel.Fatal: + if (_logger.IsFatalEnabled) + { + _logger.FatalException(messageFunc(), exception); + return true; + } + break; + default: + if (_logger.IsTraceEnabled) + { + _logger.TraceException(messageFunc(), exception); + return true; + } + break; + } + return false; + } + + private bool IsLogLevelEnable(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Debug: + return _logger.IsDebugEnabled; + case LogLevel.Info: + return _logger.IsInfoEnabled; + case LogLevel.Warn: + return _logger.IsWarnEnabled; + case LogLevel.Error: + return _logger.IsErrorEnabled; + case LogLevel.Fatal: + return _logger.IsFatalEnabled; + default: + return _logger.IsTraceEnabled; + } + } + + private object TranslateLevel(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Trace: + return _levelTrace; + case LogLevel.Debug: + return _levelDebug; + case LogLevel.Info: + return _levelInfo; + case LogLevel.Warn: + return _levelWarn; + case LogLevel.Error: + return _levelError; + case LogLevel.Fatal: + return _levelFatal; + default: + throw new ArgumentOutOfRangeException("logLevel", logLevel, null); + } + } + } + } + + internal class Log4NetLogProvider : LogProviderBase + { + private readonly Func _getLoggerByNameDelegate; + private static bool s_providerIsAvailableOverride = true; + + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "LogManager")] + public Log4NetLogProvider() + { + if (!IsLoggerAvailable()) + { + throw new InvalidOperationException("log4net.LogManager not found"); + } + _getLoggerByNameDelegate = GetGetLoggerMethodCall(); + } + + public static bool ProviderIsAvailableOverride + { + get { return s_providerIsAvailableOverride; } + set { s_providerIsAvailableOverride = value; } + } + + public override Logger GetLogger(string name) + { + return new Log4NetLogger(_getLoggerByNameDelegate(name)).Log; + } + + internal static bool IsLoggerAvailable() + { + return ProviderIsAvailableOverride && GetLogManagerType() != null; + } + + protected override OpenNdc GetOpenNdcMethod() + { + Type logicalThreadContextType = Type.GetType("log4net.LogicalThreadContext, log4net"); + PropertyInfo stacksProperty = logicalThreadContextType.GetPropertyPortable("Stacks"); + Type logicalThreadContextStacksType = stacksProperty.PropertyType; + PropertyInfo stacksIndexerProperty = logicalThreadContextStacksType.GetPropertyPortable("Item"); + Type stackType = stacksIndexerProperty.PropertyType; + MethodInfo pushMethod = stackType.GetMethodPortable("Push"); + + ParameterExpression messageParameter = + Expression.Parameter(typeof(string), "message"); + + // message => LogicalThreadContext.Stacks.Item["NDC"].Push(message); + MethodCallExpression callPushBody = + Expression.Call( + Expression.Property(Expression.Property(null, stacksProperty), + stacksIndexerProperty, + Expression.Constant("NDC")), + pushMethod, + messageParameter); + + OpenNdc result = + Expression.Lambda(callPushBody, messageParameter) + .Compile(); + + return result; + } + + protected override OpenMdc GetOpenMdcMethod() + { + Type logicalThreadContextType = Type.GetType("log4net.LogicalThreadContext, log4net"); + PropertyInfo propertiesProperty = logicalThreadContextType.GetPropertyPortable("Properties"); + Type logicalThreadContextPropertiesType = propertiesProperty.PropertyType; + PropertyInfo propertiesIndexerProperty = logicalThreadContextPropertiesType.GetPropertyPortable("Item"); + + MethodInfo removeMethod = logicalThreadContextPropertiesType.GetMethodPortable("Remove"); + + ParameterExpression keyParam = Expression.Parameter(typeof(string), "key"); + ParameterExpression valueParam = Expression.Parameter(typeof(string), "value"); + + MemberExpression propertiesExpression = Expression.Property(null, propertiesProperty); + + // (key, value) => LogicalThreadContext.Properties.Item[key] = value; + BinaryExpression setProperties = Expression.Assign(Expression.Property(propertiesExpression, propertiesIndexerProperty, keyParam), valueParam); + + // key => LogicalThreadContext.Properties.Remove(key); + MethodCallExpression removeMethodCall = Expression.Call(propertiesExpression, removeMethod, keyParam); + + Action set = Expression + .Lambda>(setProperties, keyParam, valueParam) + .Compile(); + + Action remove = Expression + .Lambda>(removeMethodCall, keyParam) + .Compile(); + + return (key, value) => + { + set(key, value); + return new DisposableAction(() => remove(key)); + }; + } + + private static Type GetLogManagerType() + { + return Type.GetType("log4net.LogManager, log4net"); + } + + private static Func GetGetLoggerMethodCall() + { + Type logManagerType = GetLogManagerType(); + MethodInfo method = logManagerType.GetMethodPortable("GetLogger", typeof(string)); + ParameterExpression nameParam = Expression.Parameter(typeof(string), "name"); + MethodCallExpression methodCall = Expression.Call(null, method, nameParam); + return Expression.Lambda>(methodCall, nameParam).Compile(); + } + + internal class Log4NetLogger + { + private readonly dynamic _logger; + private static Type s_callerStackBoundaryType; + private static readonly object CallerStackBoundaryTypeSync = new object(); + + private readonly object _levelDebug; + private readonly object _levelInfo; + private readonly object _levelWarn; + private readonly object _levelError; + private readonly object _levelFatal; + private readonly Func _isEnabledForDelegate; + private readonly Action _logDelegate; + private readonly Func _createLoggingEvent; + private Action _loggingEventPropertySetter; + + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "ILogger")] + internal Log4NetLogger(dynamic logger) + { + _logger = logger.Logger; + + var logEventLevelType = Type.GetType("log4net.Core.Level, log4net"); + if (logEventLevelType == null) + { + throw new InvalidOperationException("Type log4net.Core.Level was not found."); + } + + var levelFields = logEventLevelType.GetFieldsPortable().ToList(); + _levelDebug = levelFields.First(x => x.Name == "Debug").GetValue(null); + _levelInfo = levelFields.First(x => x.Name == "Info").GetValue(null); + _levelWarn = levelFields.First(x => x.Name == "Warn").GetValue(null); + _levelError = levelFields.First(x => x.Name == "Error").GetValue(null); + _levelFatal = levelFields.First(x => x.Name == "Fatal").GetValue(null); + + // Func isEnabledFor = (logger, level) => { return ((log4net.Core.ILogger)logger).IsEnabled(level); } + var loggerType = Type.GetType("log4net.Core.ILogger, log4net"); + if (loggerType == null) + { + throw new InvalidOperationException("Type log4net.Core.ILogger, was not found."); + } + ParameterExpression instanceParam = Expression.Parameter(typeof(object)); + UnaryExpression instanceCast = Expression.Convert(instanceParam, loggerType); + ParameterExpression levelParam = Expression.Parameter(typeof(object)); + UnaryExpression levelCast = Expression.Convert(levelParam, logEventLevelType); + _isEnabledForDelegate = GetIsEnabledFor(loggerType, logEventLevelType, instanceCast, levelCast, instanceParam, levelParam); + + Type loggingEventType = Type.GetType("log4net.Core.LoggingEvent, log4net"); + + _createLoggingEvent = GetCreateLoggingEvent(instanceParam, instanceCast, levelParam, levelCast, loggingEventType); + + _logDelegate = GetLogDelegate(loggerType, loggingEventType, instanceCast, instanceParam); + + _loggingEventPropertySetter = GetLoggingEventPropertySetter(loggingEventType); + } + + private static Action GetLogDelegate(Type loggerType, Type loggingEventType, UnaryExpression instanceCast, + ParameterExpression instanceParam) + { + //Action Log = + //(logger, callerStackBoundaryDeclaringType, level, message, exception) => { ((ILogger)logger).Log(new LoggingEvent(callerStackBoundaryDeclaringType, logger.Repository, logger.Name, level, message, exception)); } + MethodInfo writeExceptionMethodInfo = loggerType.GetMethodPortable("Log", + loggingEventType); + + ParameterExpression loggingEventParameter = + Expression.Parameter(typeof(object), "loggingEvent"); + + UnaryExpression loggingEventCasted = + Expression.Convert(loggingEventParameter, loggingEventType); + + var writeMethodExp = Expression.Call( + instanceCast, + writeExceptionMethodInfo, + loggingEventCasted); + + var logDelegate = Expression.Lambda>( + writeMethodExp, + instanceParam, + loggingEventParameter).Compile(); + + return logDelegate; + } + + private static Func GetCreateLoggingEvent(ParameterExpression instanceParam, UnaryExpression instanceCast, ParameterExpression levelParam, UnaryExpression levelCast, Type loggingEventType) + { + ParameterExpression callerStackBoundaryDeclaringTypeParam = Expression.Parameter(typeof(Type)); + ParameterExpression messageParam = Expression.Parameter(typeof(string)); + ParameterExpression exceptionParam = Expression.Parameter(typeof(Exception)); + + PropertyInfo repositoryProperty = loggingEventType.GetPropertyPortable("Repository"); + PropertyInfo levelProperty = loggingEventType.GetPropertyPortable("Level"); + + ConstructorInfo loggingEventConstructor = + loggingEventType.GetConstructorPortable(typeof(Type), repositoryProperty.PropertyType, typeof(string), levelProperty.PropertyType, typeof(object), typeof(Exception)); + + //Func Log = + //(logger, callerStackBoundaryDeclaringType, level, message, exception) => new LoggingEvent(callerStackBoundaryDeclaringType, ((ILogger)logger).Repository, ((ILogger)logger).Name, (Level)level, message, exception); } + NewExpression newLoggingEventExpression = + Expression.New(loggingEventConstructor, + callerStackBoundaryDeclaringTypeParam, + Expression.Property(instanceCast, "Repository"), + Expression.Property(instanceCast, "Name"), + levelCast, + messageParam, + exceptionParam); + + var createLoggingEvent = + Expression.Lambda>( + newLoggingEventExpression, + instanceParam, + callerStackBoundaryDeclaringTypeParam, + levelParam, + messageParam, + exceptionParam) + .Compile(); + + return createLoggingEvent; + } + + private static Func GetIsEnabledFor(Type loggerType, Type logEventLevelType, + UnaryExpression instanceCast, + UnaryExpression levelCast, + ParameterExpression instanceParam, + ParameterExpression levelParam) + { + MethodInfo isEnabledMethodInfo = loggerType.GetMethodPortable("IsEnabledFor", logEventLevelType); + MethodCallExpression isEnabledMethodCall = Expression.Call(instanceCast, isEnabledMethodInfo, levelCast); + + Func result = + Expression.Lambda>(isEnabledMethodCall, instanceParam, levelParam) + .Compile(); + + return result; + } + + private static Action GetLoggingEventPropertySetter(Type loggingEventType) + { + ParameterExpression loggingEventParameter = Expression.Parameter(typeof(object), "loggingEvent"); + ParameterExpression keyParameter = Expression.Parameter(typeof(string), "key"); + ParameterExpression valueParameter = Expression.Parameter(typeof(object), "value"); + + PropertyInfo propertiesProperty = loggingEventType.GetPropertyPortable("Properties"); + PropertyInfo item = propertiesProperty.PropertyType.GetPropertyPortable("Item"); + + // ((LoggingEvent)loggingEvent).Properties[key] = value; + var body = + Expression.Assign( + Expression.Property( + Expression.Property(Expression.Convert(loggingEventParameter, loggingEventType), + propertiesProperty), item, keyParameter), valueParameter); + + Action result = + Expression.Lambda> + (body, loggingEventParameter, keyParameter, + valueParameter) + .Compile(); + + return result; + } + + public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) + { + if (messageFunc == null) + { + return IsLogLevelEnable(logLevel); + } + + if (!IsLogLevelEnable(logLevel)) + { + return false; + } + + string message = messageFunc(); + + IEnumerable patternMatches; + + string formattedMessage = + LogMessageFormatter.FormatStructuredMessage(message, + formatParameters, + out patternMatches); + + // determine correct caller - this might change due to jit optimizations with method inlining + if (s_callerStackBoundaryType == null) + { + lock (CallerStackBoundaryTypeSync) + { +#if !LIBLOG_PORTABLE + StackTrace stack = new StackTrace(); + Type thisType = GetType(); + s_callerStackBoundaryType = Type.GetType("LoggerExecutionWrapper"); + for (var i = 1; i < stack.FrameCount; i++) + { + if (!IsInTypeHierarchy(thisType, stack.GetFrame(i).GetMethod().DeclaringType)) + { + s_callerStackBoundaryType = stack.GetFrame(i - 1).GetMethod().DeclaringType; + break; + } + } +#else + s_callerStackBoundaryType = typeof(LoggerExecutionWrapper); +#endif + } + } + + var translatedLevel = TranslateLevel(logLevel); + + object loggingEvent = _createLoggingEvent(_logger, s_callerStackBoundaryType, translatedLevel, formattedMessage, exception); + + PopulateProperties(loggingEvent, patternMatches, formatParameters); + + _logDelegate(_logger, loggingEvent); + + return true; + } + + private void PopulateProperties(object loggingEvent, IEnumerable patternMatches, object[] formatParameters) + { + IEnumerable> keyToValue = + patternMatches.Zip(formatParameters, + (key, value) => new KeyValuePair(key, value)); + + foreach (KeyValuePair keyValuePair in keyToValue) + { + _loggingEventPropertySetter(loggingEvent, keyValuePair.Key, keyValuePair.Value); + } + } + + private static bool IsInTypeHierarchy(Type currentType, Type checkType) + { + while (currentType != null && currentType != typeof(object)) + { + if (currentType == checkType) + { + return true; + } + currentType = currentType.GetBaseTypePortable(); + } + return false; + } + + private bool IsLogLevelEnable(LogLevel logLevel) + { + var level = TranslateLevel(logLevel); + return _isEnabledForDelegate(_logger, level); + } + + private object TranslateLevel(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Trace: + case LogLevel.Debug: + return _levelDebug; + case LogLevel.Info: + return _levelInfo; + case LogLevel.Warn: + return _levelWarn; + case LogLevel.Error: + return _levelError; + case LogLevel.Fatal: + return _levelFatal; + default: + throw new ArgumentOutOfRangeException("logLevel", logLevel, null); + } + } + } + } + + internal class EntLibLogProvider : LogProviderBase + { + private const string TypeTemplate = "Microsoft.Practices.EnterpriseLibrary.Logging.{0}, Microsoft.Practices.EnterpriseLibrary.Logging"; + private static bool s_providerIsAvailableOverride = true; + private static readonly Type LogEntryType; + private static readonly Type LoggerType; + private static readonly Type TraceEventTypeType; + private static readonly Action WriteLogEntry; + private static readonly Func ShouldLogEntry; + + [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")] + static EntLibLogProvider() + { + LogEntryType = Type.GetType(string.Format(CultureInfo.InvariantCulture, TypeTemplate, "LogEntry")); + LoggerType = Type.GetType(string.Format(CultureInfo.InvariantCulture, TypeTemplate, "Logger")); + TraceEventTypeType = TraceEventTypeValues.Type; + if (LogEntryType == null + || TraceEventTypeType == null + || LoggerType == null) + { + return; + } + WriteLogEntry = GetWriteLogEntry(); + ShouldLogEntry = GetShouldLogEntry(); + } + + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "EnterpriseLibrary")] + public EntLibLogProvider() + { + if (!IsLoggerAvailable()) + { + throw new InvalidOperationException("Microsoft.Practices.EnterpriseLibrary.Logging.Logger not found"); + } + } + + public static bool ProviderIsAvailableOverride + { + get { return s_providerIsAvailableOverride; } + set { s_providerIsAvailableOverride = value; } + } + + public override Logger GetLogger(string name) + { + return new EntLibLogger(name, WriteLogEntry, ShouldLogEntry).Log; + } + + internal static bool IsLoggerAvailable() + { + return ProviderIsAvailableOverride + && TraceEventTypeType != null + && LogEntryType != null; + } + + private static Action GetWriteLogEntry() + { + // new LogEntry(...) + var logNameParameter = Expression.Parameter(typeof(string), "logName"); + var messageParameter = Expression.Parameter(typeof(string), "message"); + var severityParameter = Expression.Parameter(typeof(int), "severity"); + + MemberInitExpression memberInit = GetWriteLogExpression( + messageParameter, + Expression.Convert(severityParameter, TraceEventTypeType), + logNameParameter); + + //Logger.Write(new LogEntry(....)); + MethodInfo writeLogEntryMethod = LoggerType.GetMethodPortable("Write", LogEntryType); + var writeLogEntryExpression = Expression.Call(writeLogEntryMethod, memberInit); + + return Expression.Lambda>( + writeLogEntryExpression, + logNameParameter, + messageParameter, + severityParameter).Compile(); + } + + private static Func GetShouldLogEntry() + { + // new LogEntry(...) + var logNameParameter = Expression.Parameter(typeof(string), "logName"); + var severityParameter = Expression.Parameter(typeof(int), "severity"); + + MemberInitExpression memberInit = GetWriteLogExpression( + Expression.Constant("***dummy***"), + Expression.Convert(severityParameter, TraceEventTypeType), + logNameParameter); + + //Logger.Write(new LogEntry(....)); + MethodInfo writeLogEntryMethod = LoggerType.GetMethodPortable("ShouldLog", LogEntryType); + var writeLogEntryExpression = Expression.Call(writeLogEntryMethod, memberInit); + + return Expression.Lambda>( + writeLogEntryExpression, + logNameParameter, + severityParameter).Compile(); + } + + private static MemberInitExpression GetWriteLogExpression(Expression message, + Expression severityParameter, ParameterExpression logNameParameter) + { + var entryType = LogEntryType; + MemberInitExpression memberInit = Expression.MemberInit(Expression.New(entryType), + Expression.Bind(entryType.GetPropertyPortable("Message"), message), + Expression.Bind(entryType.GetPropertyPortable("Severity"), severityParameter), + Expression.Bind( + entryType.GetPropertyPortable("TimeStamp"), + Expression.Property(null, typeof(DateTime).GetPropertyPortable("UtcNow"))), + Expression.Bind( + entryType.GetPropertyPortable("Categories"), + Expression.ListInit( + Expression.New(typeof(List)), + typeof(List).GetMethodPortable("Add", typeof(string)), + logNameParameter))); + return memberInit; + } + + internal class EntLibLogger + { + private readonly string _loggerName; + private readonly Action _writeLog; + private readonly Func _shouldLog; + + internal EntLibLogger(string loggerName, Action writeLog, Func shouldLog) + { + _loggerName = loggerName; + _writeLog = writeLog; + _shouldLog = shouldLog; + } + + public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) + { + var severity = MapSeverity(logLevel); + if (messageFunc == null) + { + return _shouldLog(_loggerName, severity); + } + + + messageFunc = LogMessageFormatter.SimulateStructuredLogging(messageFunc, formatParameters); + if (exception != null) + { + return LogException(logLevel, messageFunc, exception); + } + _writeLog(_loggerName, messageFunc(), severity); + return true; + } + + public bool LogException(LogLevel logLevel, Func messageFunc, Exception exception) + { + var severity = MapSeverity(logLevel); + var message = messageFunc() + Environment.NewLine + exception; + _writeLog(_loggerName, message, severity); + return true; + } + + private static int MapSeverity(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Fatal: + return TraceEventTypeValues.Critical; + case LogLevel.Error: + return TraceEventTypeValues.Error; + case LogLevel.Warn: + return TraceEventTypeValues.Warning; + case LogLevel.Info: + return TraceEventTypeValues.Information; + default: + return TraceEventTypeValues.Verbose; + } + } + } + } + + internal class SerilogLogProvider : LogProviderBase + { + private readonly Func _getLoggerByNameDelegate; + private static bool s_providerIsAvailableOverride = true; + + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "Serilog")] + public SerilogLogProvider() + { + if (!IsLoggerAvailable()) + { + throw new InvalidOperationException("Serilog.Log not found"); + } + _getLoggerByNameDelegate = GetForContextMethodCall(); + } + + public static bool ProviderIsAvailableOverride + { + get { return s_providerIsAvailableOverride; } + set { s_providerIsAvailableOverride = value; } + } + + public override Logger GetLogger(string name) + { + return new SerilogLogger(_getLoggerByNameDelegate(name)).Log; + } + + internal static bool IsLoggerAvailable() + { + return ProviderIsAvailableOverride && GetLogManagerType() != null; + } + + protected override OpenNdc GetOpenNdcMethod() + { + return message => GetPushProperty()("NDC", message); + } + + protected override OpenMdc GetOpenMdcMethod() + { + return (key, value) => GetPushProperty()(key, value); + } + + private static Func GetPushProperty() + { + Type ndcContextType = Type.GetType("Serilog.Context.LogContext, Serilog") ?? + Type.GetType("Serilog.Context.LogContext, Serilog.FullNetFx"); + + MethodInfo pushPropertyMethod = ndcContextType.GetMethodPortable( + "PushProperty", + typeof(string), + typeof(object), + typeof(bool)); + + ParameterExpression nameParam = Expression.Parameter(typeof(string), "name"); + ParameterExpression valueParam = Expression.Parameter(typeof(object), "value"); + ParameterExpression destructureObjectParam = Expression.Parameter(typeof(bool), "destructureObjects"); + MethodCallExpression pushPropertyMethodCall = Expression + .Call(null, pushPropertyMethod, nameParam, valueParam, destructureObjectParam); + var pushProperty = Expression + .Lambda>( + pushPropertyMethodCall, + nameParam, + valueParam, + destructureObjectParam) + .Compile(); + + return (key, value) => pushProperty(key, value, false); + } + + private static Type GetLogManagerType() + { + return Type.GetType("Serilog.Log, Serilog"); + } + + private static Func GetForContextMethodCall() + { + Type logManagerType = GetLogManagerType(); + MethodInfo method = logManagerType.GetMethodPortable("ForContext", typeof(string), typeof(object), typeof(bool)); + ParameterExpression propertyNameParam = Expression.Parameter(typeof(string), "propertyName"); + ParameterExpression valueParam = Expression.Parameter(typeof(object), "value"); + ParameterExpression destructureObjectsParam = Expression.Parameter(typeof(bool), "destructureObjects"); + MethodCallExpression methodCall = Expression.Call(null, method, new Expression[] + { + propertyNameParam, + valueParam, + destructureObjectsParam + }); + var func = Expression.Lambda>( + methodCall, + propertyNameParam, + valueParam, + destructureObjectsParam) + .Compile(); + return name => func("SourceContext", name, false); + } + + internal class SerilogLogger + { + private readonly object _logger; + private static readonly object DebugLevel; + private static readonly object ErrorLevel; + private static readonly object FatalLevel; + private static readonly object InformationLevel; + private static readonly object VerboseLevel; + private static readonly object WarningLevel; + private static readonly Func IsEnabled; + private static readonly Action Write; + private static readonly Action WriteException; + + [SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")] + [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "ILogger")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "LogEventLevel")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "Serilog")] + static SerilogLogger() + { + var logEventLevelType = Type.GetType("Serilog.Events.LogEventLevel, Serilog"); + if (logEventLevelType == null) + { + throw new InvalidOperationException("Type Serilog.Events.LogEventLevel was not found."); + } + DebugLevel = Enum.Parse(logEventLevelType, "Debug", false); + ErrorLevel = Enum.Parse(logEventLevelType, "Error", false); + FatalLevel = Enum.Parse(logEventLevelType, "Fatal", false); + InformationLevel = Enum.Parse(logEventLevelType, "Information", false); + VerboseLevel = Enum.Parse(logEventLevelType, "Verbose", false); + WarningLevel = Enum.Parse(logEventLevelType, "Warning", false); + + // Func isEnabled = (logger, level) => { return ((SeriLog.ILogger)logger).IsEnabled(level); } + var loggerType = Type.GetType("Serilog.ILogger, Serilog"); + if (loggerType == null) + { + throw new InvalidOperationException("Type Serilog.ILogger was not found."); + } + MethodInfo isEnabledMethodInfo = loggerType.GetMethodPortable("IsEnabled", logEventLevelType); + ParameterExpression instanceParam = Expression.Parameter(typeof(object)); + UnaryExpression instanceCast = Expression.Convert(instanceParam, loggerType); + ParameterExpression levelParam = Expression.Parameter(typeof(object)); + UnaryExpression levelCast = Expression.Convert(levelParam, logEventLevelType); + MethodCallExpression isEnabledMethodCall = Expression.Call(instanceCast, isEnabledMethodInfo, levelCast); + IsEnabled = Expression.Lambda>(isEnabledMethodCall, instanceParam, levelParam).Compile(); + + // Action Write = + // (logger, level, message, params) => { ((SeriLog.ILoggerILogger)logger).Write(level, message, params); } + MethodInfo writeMethodInfo = loggerType.GetMethodPortable("Write", logEventLevelType, typeof(string), typeof(object[])); + ParameterExpression messageParam = Expression.Parameter(typeof(string)); + ParameterExpression propertyValuesParam = Expression.Parameter(typeof(object[])); + MethodCallExpression writeMethodExp = Expression.Call( + instanceCast, + writeMethodInfo, + levelCast, + messageParam, + propertyValuesParam); + var expression = Expression.Lambda>( + writeMethodExp, + instanceParam, + levelParam, + messageParam, + propertyValuesParam); + Write = expression.Compile(); + + // Action WriteException = + // (logger, level, exception, message) => { ((ILogger)logger).Write(level, exception, message, new object[]); } + MethodInfo writeExceptionMethodInfo = loggerType.GetMethodPortable("Write", + logEventLevelType, + typeof(Exception), + typeof(string), + typeof(object[])); + ParameterExpression exceptionParam = Expression.Parameter(typeof(Exception)); + writeMethodExp = Expression.Call( + instanceCast, + writeExceptionMethodInfo, + levelCast, + exceptionParam, + messageParam, + propertyValuesParam); + WriteException = Expression.Lambda>( + writeMethodExp, + instanceParam, + levelParam, + exceptionParam, + messageParam, + propertyValuesParam).Compile(); + } + + internal SerilogLogger(object logger) + { + _logger = logger; + } + + public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) + { + var translatedLevel = TranslateLevel(logLevel); + if (messageFunc == null) + { + return IsEnabled(_logger, translatedLevel); + } + + if (!IsEnabled(_logger, translatedLevel)) + { + return false; + } + + if (exception != null) + { + LogException(translatedLevel, messageFunc, exception, formatParameters); + } + else + { + LogMessage(translatedLevel, messageFunc, formatParameters); + } + + return true; + } + + private void LogMessage(object translatedLevel, Func messageFunc, object[] formatParameters) + { + Write(_logger, translatedLevel, messageFunc(), formatParameters); + } + + private void LogException(object logLevel, Func messageFunc, Exception exception, object[] formatParams) + { + WriteException(_logger, logLevel, exception, messageFunc(), formatParams); + } + + private static object TranslateLevel(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Fatal: + return FatalLevel; + case LogLevel.Error: + return ErrorLevel; + case LogLevel.Warn: + return WarningLevel; + case LogLevel.Info: + return InformationLevel; + case LogLevel.Trace: + return VerboseLevel; + default: + return DebugLevel; + } + } + } + } + + internal class LoupeLogProvider : LogProviderBase + { + /// + /// The form of the Loupe Log.Write method we're using + /// + internal delegate void WriteDelegate( + int severity, + string logSystem, + int skipFrames, + Exception exception, + bool attributeToException, + int writeMode, + string detailsXml, + string category, + string caption, + string description, + params object[] args + ); + + private static bool s_providerIsAvailableOverride = true; + private readonly WriteDelegate _logWriteDelegate; + + public LoupeLogProvider() + { + if (!IsLoggerAvailable()) + { + throw new InvalidOperationException("Gibraltar.Agent.Log (Loupe) not found"); + } + + _logWriteDelegate = GetLogWriteDelegate(); + } + + /// + /// Gets or sets a value indicating whether [provider is available override]. Used in tests. + /// + /// + /// true if [provider is available override]; otherwise, false. + /// + public static bool ProviderIsAvailableOverride + { + get { return s_providerIsAvailableOverride; } + set { s_providerIsAvailableOverride = value; } + } + + public override Logger GetLogger(string name) + { + return new LoupeLogger(name, _logWriteDelegate).Log; + } + + public static bool IsLoggerAvailable() + { + return ProviderIsAvailableOverride && GetLogManagerType() != null; + } + + private static Type GetLogManagerType() + { + return Type.GetType("Gibraltar.Agent.Log, Gibraltar.Agent"); + } + + private static WriteDelegate GetLogWriteDelegate() + { + Type logManagerType = GetLogManagerType(); + Type logMessageSeverityType = Type.GetType("Gibraltar.Agent.LogMessageSeverity, Gibraltar.Agent"); + Type logWriteModeType = Type.GetType("Gibraltar.Agent.LogWriteMode, Gibraltar.Agent"); + + MethodInfo method = logManagerType.GetMethodPortable( + "Write", + logMessageSeverityType, typeof(string), typeof(int), typeof(Exception), typeof(bool), + logWriteModeType, typeof(string), typeof(string), typeof(string), typeof(string), typeof(object[])); + + var callDelegate = (WriteDelegate)method.CreateDelegate(typeof(WriteDelegate)); + return callDelegate; + } + + internal class LoupeLogger + { + private const string LogSystem = "LibLog"; + + private readonly string _category; + private readonly WriteDelegate _logWriteDelegate; + private readonly int _skipLevel; + + internal LoupeLogger(string category, WriteDelegate logWriteDelegate) + { + _category = category; + _logWriteDelegate = logWriteDelegate; +#if DEBUG + _skipLevel = 2; +#else + _skipLevel = 1; +#endif + } + + public bool Log(LogLevel logLevel, Func messageFunc, Exception exception, params object[] formatParameters) + { + if (messageFunc == null) + { + //nothing to log.. + return true; + } + + messageFunc = LogMessageFormatter.SimulateStructuredLogging(messageFunc, formatParameters); + + _logWriteDelegate(ToLogMessageSeverity(logLevel), LogSystem, _skipLevel, exception, true, 0, null, + _category, null, messageFunc.Invoke()); + + return true; + } + + private static int ToLogMessageSeverity(LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.Trace: + return TraceEventTypeValues.Verbose; + case LogLevel.Debug: + return TraceEventTypeValues.Verbose; + case LogLevel.Info: + return TraceEventTypeValues.Information; + case LogLevel.Warn: + return TraceEventTypeValues.Warning; + case LogLevel.Error: + return TraceEventTypeValues.Error; + case LogLevel.Fatal: + return TraceEventTypeValues.Critical; + default: + throw new ArgumentOutOfRangeException("logLevel"); + } + } + } + } + + internal static class TraceEventTypeValues + { + internal static readonly Type Type; + internal static readonly int Verbose; + internal static readonly int Information; + internal static readonly int Warning; + internal static readonly int Error; + internal static readonly int Critical; + + [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")] + static TraceEventTypeValues() + { + var assembly = typeof(Uri).GetAssemblyPortable(); // This is to get to the System.dll assembly in a PCL compatible way. + if (assembly == null) + { + return; + } + Type = assembly.GetType("System.Diagnostics.TraceEventType"); + if (Type == null) return; + Verbose = (int)Enum.Parse(Type, "Verbose", false); + Information = (int)Enum.Parse(Type, "Information", false); + Warning = (int)Enum.Parse(Type, "Warning", false); + Error = (int)Enum.Parse(Type, "Error", false); + Critical = (int)Enum.Parse(Type, "Critical", false); + } + } + + internal static class LogMessageFormatter + { + //private static readonly Regex Pattern = new Regex(@"\{@?\w{1,}\}"); +#if LIBLOG_PORTABLE + private static readonly Regex Pattern = new Regex(@"(?[^\d{][^ }]*)}"); +#else + private static readonly Regex Pattern = new Regex(@"(?[^ :{}]+)(?:[^}]+)?}", RegexOptions.Compiled); +#endif + + /// + /// Some logging frameworks support structured logging, such as serilog. This will allow you to add names to structured data in a format string: + /// For example: Log("Log message to {user}", user). This only works with serilog, but as the user of LibLog, you don't know if serilog is actually + /// used. So, this class simulates that. it will replace any text in {curly braces} with an index number. + /// + /// "Log {message} to {user}" would turn into => "Log {0} to {1}". Then the format parameters are handled using regular .net string.Format. + /// + /// The message builder. + /// The format parameters. + /// + public static Func SimulateStructuredLogging(Func messageBuilder, object[] formatParameters) + { + if (formatParameters == null || formatParameters.Length == 0) + { + return messageBuilder; + } + + return () => + { + string targetMessage = messageBuilder(); + IEnumerable patternMatches; + return FormatStructuredMessage(targetMessage, formatParameters, out patternMatches); + }; + } + + private static string ReplaceFirst(string text, string search, string replace) + { + int pos = text.IndexOf(search, StringComparison.Ordinal); + if (pos < 0) + { + return text; + } + return text.Substring(0, pos) + replace + text.Substring(pos + search.Length); + } + + public static string FormatStructuredMessage(string targetMessage, object[] formatParameters, out IEnumerable patternMatches) + { + if (formatParameters.Length == 0) + { + patternMatches = Enumerable.Empty(); + return targetMessage; + } + + List processedArguments = new List(); + patternMatches = processedArguments; + + foreach (Match match in Pattern.Matches(targetMessage)) + { + var arg = match.Groups["arg"].Value; + + int notUsed; + if (!int.TryParse(arg, out notUsed)) + { + int argumentIndex = processedArguments.IndexOf(arg); + if (argumentIndex == -1) + { + argumentIndex = processedArguments.Count; + processedArguments.Add(arg); + } + + targetMessage = ReplaceFirst(targetMessage, match.Value, + "{" + argumentIndex + match.Groups["format"].Value + "}"); + } + } + try + { + return string.Format(CultureInfo.InvariantCulture, targetMessage, formatParameters); + } + catch (FormatException ex) + { + throw new FormatException("The input string '" + targetMessage + "' could not be formatted using string.Format", ex); + } + } + } + + internal static class TypeExtensions + { + internal static ConstructorInfo GetConstructorPortable(this Type type, params Type[] types) + { +#if LIBLOG_PORTABLE + return type.GetTypeInfo().DeclaredConstructors.FirstOrDefault + (constructor => + constructor.GetParameters() + .Select(parameter => parameter.ParameterType) + .SequenceEqual(types)); +#else + return type.GetConstructor(types); +#endif + } + + internal static MethodInfo GetMethodPortable(this Type type, string name) + { +#if LIBLOG_PORTABLE + return type.GetRuntimeMethods().SingleOrDefault(m => m.Name == name); +#else + return type.GetMethod(name); +#endif + } + + internal static MethodInfo GetMethodPortable(this Type type, string name, params Type[] types) + { +#if LIBLOG_PORTABLE + return type.GetRuntimeMethod(name, types); +#else + return type.GetMethod(name, types); +#endif + } + + internal static PropertyInfo GetPropertyPortable(this Type type, string name) + { +#if LIBLOG_PORTABLE + return type.GetRuntimeProperty(name); +#else + return type.GetProperty(name); +#endif + } + + internal static IEnumerable GetFieldsPortable(this Type type) + { +#if LIBLOG_PORTABLE + return type.GetRuntimeFields(); +#else + return type.GetFields(); +#endif + } + + internal static Type GetBaseTypePortable(this Type type) + { +#if LIBLOG_PORTABLE + return type.GetTypeInfo().BaseType; +#else + return type.BaseType; +#endif + } + +#if LIBLOG_PORTABLE + internal static MethodInfo GetGetMethod(this PropertyInfo propertyInfo) + { + return propertyInfo.GetMethod; + } + + internal static MethodInfo GetSetMethod(this PropertyInfo propertyInfo) + { + return propertyInfo.SetMethod; + } +#endif + +#if !LIBLOG_PORTABLE + internal static object CreateDelegate(this MethodInfo methodInfo, Type delegateType) + { + return Delegate.CreateDelegate(delegateType, methodInfo); + } +#endif + + internal static Assembly GetAssemblyPortable(this Type type) + { +#if LIBLOG_PORTABLE + return type.GetTypeInfo().Assembly; +#else + return type.Assembly; +#endif + } + } + + internal class DisposableAction : IDisposable + { + private readonly Action _onDispose; + + public DisposableAction(Action onDispose = null) + { + _onDispose = onDispose; + } + + public void Dispose() + { + if (_onDispose != null) + { + _onDispose(); + } + } + } +} diff --git a/Source/CakeMail.RestClient/Resources/Lists.cs b/Source/CakeMail.RestClient/Resources/Lists.cs index 850fd87..235e012 100644 --- a/Source/CakeMail.RestClient/Resources/Lists.cs +++ b/Source/CakeMail.RestClient/Resources/Lists.cs @@ -200,7 +200,7 @@ public Lists(IClient client) }; if (name != null) parameters.Add(new KeyValuePair("name", name)); if (language != null) parameters.Add(new KeyValuePair("language", language)); - if (spamPolicyAccepted.HasValue) parameters.Add(new KeyValuePair("list_policy", spamPolicyAccepted.Value ? "accepted" : "declined")); + if (spamPolicyAccepted.HasValue) parameters.Add(new KeyValuePair("policy", spamPolicyAccepted.Value ? "accepted" : "declined")); if (status.HasValue) parameters.Add(new KeyValuePair("status", status.Value.GetEnumMemberValue())); if (senderName != null) parameters.Add(new KeyValuePair("sender_name", senderName)); if (senderEmail != null) parameters.Add(new KeyValuePair("sender_email", senderEmail)); diff --git a/Source/CakeMail.RestClient/Utilities/CakeMailDebugHandler.cs b/Source/CakeMail.RestClient/Utilities/CakeMailDebugHandler.cs deleted file mode 100644 index 3be1288..0000000 --- a/Source/CakeMail.RestClient/Utilities/CakeMailDebugHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Pathoschild.Http.Client; -using Pathoschild.Http.Client.Extensibility; -using System.Diagnostics; - -namespace CakeMail.RestClient.Utilities -{ - /// - /// Handler for requests dispatched to the CakeMail API - /// - /// - public class CakeMailDebugHandler : IHttpFilter - { - #region PUBLIC METHODS - - /// Method invoked just before the HTTP request is submitted. This method can modify the outgoing HTTP request. - /// The HTTP request. - public void OnRequest(IRequest request) - { - var debugRequestMsg = $"Request sent to CakeMail: {request.Message.RequestUri}"; - var debugParametersMsg = $"Request parameters: {request.Message.Content.ReadAsStringAsync().Result}"; - Debug.WriteLine($"{new string('=', 25)}\r\n{debugRequestMsg}\r\n{debugParametersMsg}"); - } - - /// Method invoked just after the HTTP response is received. This method can modify the incoming HTTP response. - /// The HTTP response. - /// Whether HTTP error responses should be raised as exceptions. - public void OnResponse(IResponse response, bool httpErrorAsException) - { - var debugResponseMsg = string.Format("Response received: {0}", response.AsString().Result); - Debug.WriteLine($"\r\n{debugResponseMsg}\r\n{new string('=', 25)}"); - } - - #endregion - } -} diff --git a/Source/CakeMail.RestClient/Utilities/DiagnosticHandler.cs b/Source/CakeMail.RestClient/Utilities/DiagnosticHandler.cs new file mode 100644 index 0000000..364662d --- /dev/null +++ b/Source/CakeMail.RestClient/Utilities/DiagnosticHandler.cs @@ -0,0 +1,121 @@ +using CakeMail.RestClient.Logging; +using Pathoschild.Http.Client; +using Pathoschild.Http.Client.Extensibility; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Text; + +namespace CakeMail.RestClient.Utilities +{ + /// + /// Diagnostic handler for requests dispatched to the CakeMail API + /// + /// + internal class DiagnosticHandler : IHttpFilter + { + #region FIELDS + + private const string DIAGNOSTIC_ID_HEADER_NAME = "CakeMailRestClientDiagnosticId"; + private static readonly ILog _logger = LogProvider.For(); + private readonly IDictionary, Tuple> _diagnostics = new Dictionary, Tuple>(); + + #endregion + + #region PUBLIC METHODS + + /// Method invoked just before the HTTP request is submitted. This method can modify the outgoing HTTP request. + /// The HTTP request. + public void OnRequest(IRequest request) + { + request.WithHeader(DIAGNOSTIC_ID_HEADER_NAME, Guid.NewGuid().ToString("N")); + + var httpRequest = request.Message; + + var diagnosticMessage = new StringBuilder(); + diagnosticMessage.AppendLine($"Request: {httpRequest}"); + diagnosticMessage.AppendLine($"Request Content: {httpRequest.Content?.ReadAsStringAsync().Result ?? ""}"); + + lock (_diagnostics) + { + _diagnostics.Add(new WeakReference(request.Message), new Tuple(diagnosticMessage, Stopwatch.StartNew())); + } + } + + /// Method invoked just after the HTTP response is received. This method can modify the incoming HTTP response. + /// The HTTP response. + /// Whether HTTP error responses should be raised as exceptions. + public void OnResponse(IResponse response, bool httpErrorAsException) + { + var diagnosticInfo = GetDiagnosticInfo(response.Message.RequestMessage); + var diagnosticMessage = diagnosticInfo.Item1; + var timer = diagnosticInfo.Item2; + if (timer != null) timer.Stop(); + + var httpResponse = response.Message; + + diagnosticMessage.AppendLine($"Response: {httpResponse}"); + diagnosticMessage.AppendLine($"Response Content: {httpResponse.Content?.ReadAsStringAsync().Result ?? ""}"); + + if (timer != null) + { + diagnosticMessage.AppendLine($"The request took {timer.Elapsed.ToDurationString()}"); + } + + Debug.WriteLine("{0}\r\n{1}{0}", new string('=', 25), diagnosticMessage.ToString()); + + if (_logger != null && _logger.IsDebugEnabled()) + { + _logger.Debug(diagnosticMessage.ToString() + .Replace("{", "{{") + .Replace("}", "}}")); + } + } + + #endregion + + #region PRIVATE METHODS + + private Tuple GetDiagnosticInfo(HttpRequestMessage requestMessage) + { + lock (_diagnostics) + { + var diagnosticId = requestMessage.Headers.GetValues(DIAGNOSTIC_ID_HEADER_NAME).FirstOrDefault(); + + foreach (WeakReference key in _diagnostics.Keys.ToArray()) + { + // Get request + HttpRequestMessage request; + + // Check if garbage collected + if (!key.TryGetTarget(out request)) + { + _diagnostics.Remove(key); + continue; + } + + // Check if different request + var requestDiagnosticId = request.Headers.GetValues(DIAGNOSTIC_ID_HEADER_NAME).FirstOrDefault(); + if (requestDiagnosticId != diagnosticId) + { + continue; + } + + // Get the diagnostic info from dictionary + var diagnosticInfo = _diagnostics[key]; + + // Remove the diagnostic info from dictionary + _diagnostics.Remove(key); + + return diagnosticInfo; + } + } + + return new Tuple(new StringBuilder(), null); + } + + #endregion + } +} diff --git a/Source/CakeMail.RestClient/Utilities/ExtensionMethods.cs b/Source/CakeMail.RestClient/Utilities/ExtensionMethods.cs index 31efe3b..ac0410b 100644 --- a/Source/CakeMail.RestClient/Utilities/ExtensionMethods.cs +++ b/Source/CakeMail.RestClient/Utilities/ExtensionMethods.cs @@ -7,6 +7,7 @@ using System.Net.Http; using System.Reflection; using System.Runtime.Serialization; +using System.Text; using System.Threading.Tasks; namespace CakeMail.RestClient.Utilities @@ -107,6 +108,37 @@ public static IRequest WithFormUrlEncodedBody(this IRequest request, IEnumerable return request.WithBodyContent(body); } + /// + /// Converts the value of the current System.TimeSpan object to its equivalent string + /// representation by using a human readable format. + /// + /// The time span. + /// Returns the human readable representation of the TimeSpan + public static string ToDurationString(this TimeSpan timeSpan) + { + // In case the TimeSpan is extremely short + if (timeSpan.TotalMilliseconds <= 1) return "1 millisecond"; + + var result = new StringBuilder(); + + if (timeSpan.Days == 1) result.Append(" 1 day"); + else if (timeSpan.Days > 1) result.AppendFormat(" {0} days", timeSpan.Days); + + if (timeSpan.Hours == 1) result.Append(" 1 hour"); + else if (timeSpan.Hours > 1) result.AppendFormat(" {0} hours", timeSpan.Hours); + + if (timeSpan.Minutes == 1) result.Append(" 1 minute"); + else if (timeSpan.Minutes > 1) result.AppendFormat(" {0} minutes", timeSpan.Minutes); + + if (timeSpan.Seconds == 1) result.Append(" 1 second"); + else if (timeSpan.Seconds > 1) result.AppendFormat(" {0} seconds", timeSpan.Seconds); + + if (timeSpan.Milliseconds == 1) result.Append(" 1 millisecond"); + else if (timeSpan.Milliseconds > 1) result.AppendFormat(" {0} milliseconds", timeSpan.Milliseconds); + + return result.ToString().Trim(); + } + /// Asynchronously parses the JSON response from the CakeMail API and converts the data the desired type. /// The response model to deserialize into. /// The content diff --git a/Source/CakeMail.RestClient/project.json b/Source/CakeMail.RestClient/project.json index ca11dff..2bb6e2a 100644 --- a/Source/CakeMail.RestClient/project.json +++ b/Source/CakeMail.RestClient/project.json @@ -28,17 +28,16 @@ "frameworks": { "netstandard1.3": { "buildOptions": { - "define": [ "NETSTANDARD" ] + "define": [ "NETSTANDARD", "LIBLOG_PORTABLE" ] }, "imports": "dnxcore50", "dependencies": { - "NETStandard.Library": "1.6.1", - "System.Reflection.TypeExtensions": "4.3.0" + "NETStandard.Library": "1.6.1" } }, "net452": { "buildOptions": { - "define": [ "NETFULL" ] + "define": [ "NETFULL", "LIBLOG_PORTABLE" ] }, "dependencies": { }, @@ -47,5 +46,5 @@ } } }, - "version": "7.0.0-*" + "version": "7.2.0-*" } diff --git a/build.cake b/build.cake index 30ae192..f982d4f 100644 --- a/build.cake +++ b/build.cake @@ -2,12 +2,12 @@ #addin "nuget:?package=Cake.Coveralls&version=0.4.0" // Install tools. -#tool "nuget:?package=GitVersion.CommandLine&version=4.0.0-beta0009" +#tool "nuget:?package=GitVersion.CommandLine&version=4.0.0-beta0012" #tool "nuget:?package=GitReleaseManager&version=0.6.0" #tool "nuget:?package=OpenCover&version=4.6.519" -#tool "nuget:?package=ReportGenerator&version=2.5.2" +#tool "nuget:?package=ReportGenerator&version=2.5.8" #tool "nuget:?package=coveralls.io&version=1.3.4" -#tool "nuget:?package=xunit.runner.console&version=2.2.0-rc1-build3507" +#tool "nuget:?package=xunit.runner.console&version=2.2.0" /////////////////////////////////////////////////////////////////////////////// @@ -25,7 +25,7 @@ var configuration = Argument("configuration", "Release"); var libraryName = "CakeMail.RestClient"; var gitHubRepo = "CakeMail.RestClient"; -var testCoverageFilter = "+[CakeMail.RestClient]* -[CakeMail.RestClient]CakeMail.RestClient.Properties.* -[CakeMail.RestClient]CakeMail.RestClient.Models.*"; +var testCoverageFilter = "+[CakeMail.RestClient]* -[CakeMail.RestClient]CakeMail.RestClient.Properties.* -[CakeMail.RestClient]CakeMail.RestClient.Models.* -[CakeMail.RestClient]CakeMail.RestClient.Logging.*"; var testCoverageExcludeByAttribute = "*.ExcludeFromCodeCoverage*"; var testCoverageExcludeByFile = "*/*Designer.cs;*/*AssemblyInfo.cs"; @@ -211,18 +211,14 @@ Task("Run-Code-Coverage") .IsDependentOn("Build") .Does(() => { - var testSettings = new DotNetCoreTestSettings { - Configuration = configuration - }; - Action testAction = ctx => ctx.DotNetCoreTest(unitTestsPath, new DotNetCoreTestSettings { NoBuild = true, Configuration = configuration, - ArgumentCustomization = args => args.AppendSwitchQuoted("-xml", codeCoverageDir + "/coverage.xml") + ArgumentCustomization = args => args.AppendSwitchQuoted("-xml", codeCoverageDir + "coverage.xml") }); OpenCover(testAction, - codeCoverageDir + "/coverage.xml", + codeCoverageDir + "coverage.xml", new OpenCoverSettings { ArgumentCustomization = args => args.Append("-mergeoutput") } @@ -328,7 +324,7 @@ Task("Create-Release-Notes") GitReleaseManagerCreate(gitHubUserName, gitHubPassword, gitHubUserName, gitHubRepo, new GitReleaseManagerCreateSettings { Name = milestone, Milestone = milestone, - Prerelease = true, + Prerelease = false, TargetCommitish = "master" }); }); diff --git a/nuspec/CakeMail.RestClient.nuspec b/nuspec/CakeMail.RestClient.nuspec index 32291fe..0ec79b6 100644 --- a/nuspec/CakeMail.RestClient.nuspec +++ b/nuspec/CakeMail.RestClient.nuspec @@ -17,14 +17,13 @@ CakeMail API REST - + -