Search code examples
c#.netasp.net-mvcconfigurationtracesource

Asp.NET MVC, custom TextWriterTraceListener does not create a file


For MVC application custom listener does not create a log file when initializeData="CustomWeblog.txt" parameter is used, but initializeData="d:\CustomWeblog.txt" triggers file creation. What is the reason of such behaviour? Console application generates files for all types of listeners.

Custom class:

public class CustomTextWriterTraceListener : TextWriterTraceListener 
{ 
     public CustomTextWriterTraceListener(string fileName) : base(fileName)
}

Web.config (mvc application, web.config)

<trace autoflush="true" />
<sources>
  <source name="Trace">    
      <listeners>
        <add name="TextWriterListner"
             type="System.Diagnostics.TextWriterTraceListener, WebTracing" initializeData="Weblog.txt"/>
        <!-- the file is created -->
        <add name="CustomTextWriterListner"
             type="WebTracing.CustomTextWriterTraceListener, WebTracing" initializeData="CustomWeblog.txt"/>
        <!-- the file is not created in MVC application ?! -->
        <add name="CustomTextWriterListnerAbsolutePath"
             type="WebTracing.CustomTextWriterTraceListener, WebTracing" initializeData="d:\CustomWeblog.txt"/>
        <!-- the file is created -->
      </listeners>      
  </source>
</sources>

Cutom listener does not create a log file.

Caller:

TraceSource obj = new TraceSource("Trace", SourceLevels.All);
obj.TraceEvent(TraceEventType.Critical,0,"This is a critical message");

I have tried to add some extra configuration: from this blog and this one. But there is no success. Should I provide a absolute path? Is there any workaround by creating a separate assembly for custom listener?


Solution

  • I was trying to create my own rolling text writer trace listener when I was encountering the same issue you described. Long story short, after all the running around here is what I came up with.

    public class RollingTextWriterTraceListener : TextWriterTraceListener {
        string fileName;
        private static string[] _supportedAttributes = new string[] 
            { 
                "template", "Template", 
                "convertWriteToEvent", "ConvertWriteToEvent",
                "addtoarchive","addToArchive","AddToArchive",
            };
    
        public RollingTextWriterTraceListener(string fileName)
            : base() {
            this.fileName = fileName;
        }
        /// <summary>
        /// This makes sure that the writer exists to be written to.
        /// </summary>
        private void ensureWriter() {
            //Resolve file name given. relative paths (if present) are resolved to full paths.
            // Also allows for paths like this: initializeData="~/Logs/{ApplicationName}_{DateTime:yyyy-MM-dd}.log"
            var logFileFullPath = ServerPathUtility.ResolvePhysicalPath(fileName);
            var writer = base.Writer;
            if (writer == null && createWriter(logFileFullPath)) {
                writer = base.Writer;
            }
            if (!File.Exists(logFileFullPath)) {
                if (writer != null) {
                    try {
                        writer.Flush();
                        writer.Close();
                        writer.Dispose();
                    } catch (ObjectDisposedException) { }
                }
                createWriter(logFileFullPath);
            }
            //Custom code to package the previous log file(s) into a zip file.
            if (AddToArchive) {
                TextFileArchiveHelper.Archive(logFileFullPath);
            }
        }
    
        bool createWriter(string logFileFullPath) {
            try {
                logFileFullPath = ServerPathUtility.ResolveOrCreatePath(logFileFullPath);
                var writer = new StreamWriter(logFileFullPath, true);
                base.Writer = writer;
                return true;
            } catch (IOException) {
                //locked as already in use
                return false;
            } catch (UnauthorizedAccessException) {
                //ERROR_ACCESS_DENIED, mostly ACL issues
                return false;
            }
        }
    
        /// <summary>
        /// Get the add to archive flag
        /// </summary>
        public bool AddToArchive {
            get {
                // Default behaviour is not to add to archive.
                var addToArchive = false;
                var key = Attributes.Keys.Cast<string>().
                    FirstOrDefault(s => string.Equals(s, "addtoarchive", StringComparison.InvariantCultureIgnoreCase));
                if (!string.IsNullOrWhiteSpace(key)) {
                    bool.TryParse(Attributes[key], out addToArchive);
                }
                return addToArchive;
            }
        }
    
        #region Overrides
        /// <summary>
        /// Allowed attributes for this trace listener.
        /// </summary>
        protected override string[] GetSupportedAttributes() {
            return _supportedAttributes;
        }
    
        public override void Flush() {
            ensureWriter();
            base.Flush();
        }
    
        public override void Write(string message) {
            ensureWriter();
            base.Write(message);
        }
    
        public override void WriteLine(string message) {
            ensureWriter();
            base.WriteLine(message);
        }
        #endregion
    }
    

    UPDATE: Here is the utility class I wrote for resolving paths.

    public static class ServerPathUtility {
    
        public static string ResolveOrCreatePath(string pathToReplace) {
            string rootedFileName = ResolvePhysicalPath(pathToReplace);
            FileInfo fi = new FileInfo(rootedFileName);
            try {
                DirectoryInfo di = new DirectoryInfo(fi.DirectoryName);
                if (!di.Exists) {
                    di.Create();
                }
                if (!fi.Exists) {
                    fi.CreateText().Close();
                }
            } catch {
                // NO-OP
                // TODO: Review what should be done here.
            }
            return fi.FullName;
        }
    
        public static string ResolvePhysicalPath(string pathToReplace) {
            string rootedPath = ResolveFormat(pathToReplace);
            if (rootedPath.StartsWith("~") || rootedPath.StartsWith("/")) {
                rootedPath = System.Web.Hosting.HostingEnvironment.MapPath(rootedPath);
            } else if (!Path.IsPathRooted(rootedPath)) {
                rootedPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, rootedPath);
            }
            return rootedPath;
        }
    
        public static string ResolveFormat(string format) {
            string result = format;
    
            try {
                result = ExpandApplicationVariables(format);
            } catch (System.Security.SecurityException) {
                // Log?
            }
    
            try {
                string variables = Environment.ExpandEnvironmentVariables(result);
                // If an Environment Variable is not found then remove any invalid tokens
                Regex filter = new Regex("%(.*?)%", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
    
                string filePath = filter.Replace(variables, "");
    
                if (Path.GetDirectoryName(filePath) == null) {
                    filePath = Path.GetFileName(filePath);
                }
                result = filePath;
            } catch (System.Security.SecurityException) {
                // Log?
            }
    
            return result;
        }
    
        public static string ExpandApplicationVariables(string input) {
            var filter = new Regex("{(.*?)}", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
            var result = filter.Replace(input, evaluateMatch());
            return result;
        }
    
        private static MatchEvaluator evaluateMatch() {
            return match => {
                var variableName = match.Value;
                var value = GetApplicationVariable(variableName);
                return value;
            };
        }
    
        public static string GetApplicationVariable(string variable) {
            string value = string.Empty;
            variable = variable.Replace("{", "").Replace("}", "");
            var parts = variable.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
            variable = parts[0];
            var parameter = string.Empty;
            if (parts.Length > 1) {
                parameter = string.Join("", parts.Skip(1));
            }
    
            Func<string, string> resolve = null;
            value = VariableResolutionStrategies.TryGetValue(variable.ToUpperInvariant(), out resolve) && resolve != null
                ? resolve(parameter)
                : string.Empty;
    
            return value;
        }
    
        public static readonly IDictionary<string, Func<string, string>> VariableResolutionStrategies =
            new Dictionary<string, Func<string, string>> {
                {"MACHINENAME", p => Environment.MachineName },
                {"APPDOMAIN", p => AppDomain.CurrentDomain.FriendlyName },
                {"DATETIME", getDate},
                {"DATE", getDate},
                {"UTCDATETIME", getUtcDate},
                {"UTCDATE", getUtcDate},
            };
    
        static string getDate(string format = "yyyy-MM-dd") {
            var value = string.Empty;
            if (string.IsNullOrWhiteSpace(format))
                format = "yyyy-MM-dd";
            value = DateTime.Now.ToString(format);
            return value;
        }
    
        static string getUtcDate(string format = "yyyy-MM-dd") {
            var value = string.Empty;
            if (string.IsNullOrWhiteSpace(format))
                format = "yyyy-MM-dd";
            value = DateTime.Now.ToString(format);
            return value;
        }
    }
    

    So this utility class allows me to resolve relative paths and also customize formats. For example, if you looked at the code you would have seen that application name ApplicationName variable does not exist in this path

    "~/Logs/{ApplicationName}_{DateTime:yyyy-MM-dd}.log"
    

    I am able to configure that in the startup of the application along with any other variables I want to add like so

    public partial class Startup {
        public void Configuration(IAppBuilder app) {
            //... Code removed for brevity           
            // Add APPLICATIONNAME name to path Utility
            ServerPathUtility.VariableResolutionStrategies["APPLICATIONNAME"] = p => {
                var assembly = System.Reflection.Assembly.GetExecutingAssembly();
                if (assembly != null)
                    return assembly.GetName().Name;
                return string.Empty;
            };           
        }
    }