Monday, September 11, 2017

Sitecore AutoPublish Module

Over the years, many Sitecore clients have asked for the ability to have better control over Sitecore's publishing, specifically when it comes to when and what gets published. I am proud to announce that there is now a Sitecore module for just that purpose.

The module is called Mindshift.SC.AutoPublish and is now available in the Sitecore Shared Source here:

It has been added to Github as part of the larger Mindshift.SC project here.
(note: this also contains the AdoLogger previously discussed on this blog)

The latest release can be found in the Releases folder here:
https://github.com/mindSHIFT-AppDev/Mindshift.SC/blob/master/Releases/

Installation

To install, simply install the update package via Sitecore's Update Installation Wizard.

Usage

Once installed, you can create new Publish Schedules under the following path:
/sitecore/system/Modules/Mindshift SC/Auto Publish/Publish Schedules

Fields

Each Publish Schedule contains the following fields:


  • Frequency - this defines how often the publish should run: Daily, Hourly, Monthly, Once, Weekly.

Based on the selected Frequency, one one or more of the following options become available:


  • Specific Date And Time
  • Day of the Month
  • Day of the Week
  • Time of the Day
  • Days of the Week

The following fields are available for all frequencies:


  • Schedule Start Date and Schedule End Date - the schedule will only be active within these two dates. Both are optional.
  • Root Path - the path which will be published
  • Include Children - when selected, all children will also be published. If not selected, then it will only publish the root item.
  • Publishing Targets - the Sitecore targets with which to publish. Important: this is required. If need to select at least one.
  • Enabled - uncheck this to disable to Publish Scehdule completely, regardless of any other setting.
  • Languages - the specific languages to publish. This defaults to "en" if there are no entries.


Wednesday, July 5, 2017

Sitecore log4net ADO Logging Part 2: The ASR

Sitecore log4net ADO Logging Part 2: The ASR This is part of a two part post on logging Sitecore to a database using ADO.
This post is split into two parts:
Now that I have a log of all of the errors, it was time for me to come up with a way to view it. My first instinct was to create a custom angular listing page - and I still may do this - but the easier way was to use something existing and the Advanced System Reporter was a good fit.

The Code

The first step (after, of course, installing the ASR package) was to create a custom Scanner. To do this, I inherited from the BaseScanner, as follows:
    public class AdoNetLogScanner : ASR.Interface.BaseScanner {
Created a reference to the Sitecore SqlServerApi (using our log4net connection string):
        private SqlServerDataApi dataApi = new SqlServerDataApi(
            Sitecore.Configuration.Settings.GetConnectionString("log4net")
        );
Then overriding the Scan method:
public override System.Collections.ICollection Scan() {
I can then return a list of objects. In this case I created a reader and iterated through creating result LogItem instances:
string query = @"SELECT [ID], [Date], [Thread], [Level], [Logger], [Message], [Exception], [MachineName], [CurrentUser], [Roles],  [SitecoreItemID], [SitecoreItemName], [Language], [IpAddress], [ForwardedIpAddress], [HttpReferrer], [HttpUrl], [HttpMethod], [FormVariables], [HttpUserAgent], [HttpQueryString], [HttpCookies]     from dbo.[log]";

    string whereClause = "";
    if (!string.IsNullOrEmpty(Level)) {
    whereClause += string.Format(" and [Level] = '{0}'", Level);
    }

    if (!string.IsNullOrEmpty(FromDate)) {
        whereClause += string.Format(" and [Date] >= '{0} 00:00:00.000'", FromDate);
    }

    if (!string.IsNullOrEmpty(ToDate)) {
        whereClause += string.Format(" and [Date] <= '{0} 23:59:59.999'", ToDate);
    }

    if (whereClause.Length > 0) {
whereClause = " where " + whereClause.Substring(5, whereClause.Length - 5);
}

Sitecore.Data.DataProviders.Sql.DataProviderReader reader =
dataApi.CreateReader(query + whereClause + " order by [Date] desc"); //,

    List<LogItem> resultList = new List<LogItem>();
while (reader.Read()) {
        var result = new LogItem(
            reader.InnerReader.GetInt64(0),
            reader.InnerReader.GetDateTime(1),
            reader.InnerReader.GetString(2),
            reader.InnerReader.GetString(3),
            reader.InnerReader.GetString(4),
            reader.InnerReader.GetString(5),
            reader.InnerReader.GetString(6),
            reader.InnerReader.GetString(7),
            reader.InnerReader.GetString(8),
            reader.InnerReader.GetString(9),
            reader.InnerReader.GetString(10),
            reader.InnerReader.GetString(11),
            reader.InnerReader.GetString(12),
            reader.InnerReader.GetString(13),
            reader.InnerReader.GetString(14),
            reader.InnerReader.GetString(15),
            reader.InnerReader.GetString(16),
            reader.InnerReader.GetString(17),
            reader.InnerReader.GetString(18),
            reader.InnerReader.GetString(19),
            reader.InnerReader.GetString(20),
            reader.InnerReader.GetString(21)
        );
        resultList.Add(result);
    }
    return resultList;
The LogItem is just a simple POCO:
    public class LogItem {
        public long Id { get; private set; }
        public DateTime Date { get; private set; }
        public string Thread { get; private set; }
        public string Level { get; private set; }
        public string Logger { get; private set; }
        public string Message { get; protected set; }
        public string Exception { get; protected set; }
        public string MachineName { get; private set; }
        public string CurrentUser { get; private set; }
        public string Roles { get; private set; }
        public string SitecoreItemId { get; private set; }
        public string SitecoreItemName { get; private set; }
        public string Language { get; private set; }
        public string IpAddress { get; private set; }
        public string ForwardedIpAddress { get; private set; }
        public string HttpReferrer { get; private set; }
        public string HttpUrl { get; private set; }
        public string HttpMethod { get; private set; }
        public string FormVariables { get; private set; }
        public string HttpUserAgent { get; private set; }
        public string HttpQueryString { get; private set; }
        public string HttpCookies { get; private set; }

        public LogItem(long id, DateTime date, string thread, string level, string logger,
string message, string exception, string machineName, string currentUser,
string roles, string sitecoreItemId, string sitecoreItemName, 
string language, string ipAddress, string forwardedIpAddress, 
string httpReferrer, string httpUrl, string httpMethod,
        string formVariables, string httpUserAgent, string httpQueryString, 
string httpCookies) {
            Id = id;
            Date = date;
            Thread = thread;
            Level = level;
            Message = message;
            Exception = exception;
            Logger = logger;
            MachineName = machineName;
            CurrentUser = currentUser;
            Roles = roles;
            SitecoreItemId = sitecoreItemId;
            Language = language;
            IpAddress = ipAddress;
            ForwardedIpAddress = forwardedIpAddress;
            HttpReferrer = httpReferrer;
            HttpUrl = httpUrl;
            HttpMethod = httpMethod;
            FormVariables = formVariables;
            HttpUserAgent = httpUserAgent;
            HttpQueryString = httpQueryString;
            HttpCookies = httpCookies;
        }
    }
Next we need a Viewer. Feel free to add any columns or additional icons:
public class AdoNetLogViewer : ASR.Interface.BaseViewer {
    private readonly string ICON_WARN = "Applications/32x32/warning.png";
    private readonly string ICON_ERROR = "Applications/32x32/delete.png";
    private readonly string ICON_INFO = "Applications/32x32/information2.png";
    private readonly string ICON_AUDIT = "Applications/32x32/scroll_view.png";

    public override string[] AvailableColumns {
get { 
return new string[] { 
"Id", "Level", "Date", "Message", "User", "SitecoreItemId"
}; 
}
    }

    public override void Display(DisplayElement dElement) {
        Debug.ArgumentNotNull(dElement, "element");

        var logElement = dElement.Element as LogItem;
        if (logElement == null) return;

        dElement.Icon = GetIcon(logElement);

        foreach (var column in Columns) {
            switch (column.Name) {
                case "id":
                    dElement.AddColumn(column.Header, logElement.Id.ToString());
                    break;
                case "level":
                    dElement.AddColumn(column.Header, logElement.Level.ToString());
                    break;
                case "date":
                    dElement.AddColumn(column.Header, logElement.Date.ToString("yyyy-MM-dd HH:mm:ss"));
                    break;
                case "message":
                    dElement.AddColumn(column.Header, logElement.Message.Substring(0, Math.Min(logElement.Message.Length, 200)));
                    break;
                case "user":
                    dElement.AddColumn(column.Header, logElement.CurrentUser);
                    break;
                case "sitecoreitemid":
                    dElement.AddColumn(column.Header, logElement.SitecoreItemId);
                    break;
            }
        }
        dElement.Value = logElement.Id.ToString();
    }

    private string GetIcon(LogItem logElement) {
        switch (logElement.Level) {
            case "AUDIT":
                return ICON_AUDIT;
            case "WARN":
                return ICON_WARN;
            case "INFO":
                return ICON_INFO;
            case "ERROR":
                return ICON_ERROR;
        }
        return string.Empty;
    }
}

The Sitecore Changes

Create a new item for the Scanner:
/sitecore/system/Modules/ASR/Configuration/Scanners/Ado Net Log Scanner
Assembly: Mindshift.SC.AdoLogging
Class: Mindshift.SC.AdoLogging.Reporting.AdoNetLogScanner
Attributes: Level={LogLevel}|FromDate={FromDate}|ToDate={ToDate}
Create a new item for the Viewer:
Item: /sitecore/system/Modules/ASR/Configuration/Viewer/Ado Net Log Viewer
Assembly: Mindshift.SC.AdoLogging
Class: Mindshift.SC.AdoLogging.Reporting.AdoNetLogViewer
Attributes: 
Columns:
* Id - id
* Level - level
* Date - date
* Message - message
* User - user
* Sitecore Item Id - sitecoreitemid
Create a new item for the Report itself:
/sitecore/system/Modules/ASR/Reports/Ado Net Log
Then choose the Scanner and Viewer from above.
You should then be able to run the report and see your records.

Thursday, June 29, 2017

Sitecore log4net ADO Logging Part 1: The Appender

This details how to create an ADO.NET log appender for log4net in Sitecore.
This post is split into two parts:
This will cover the first part.

A Note of Namespace

You will need a reference to Sitecore.Logging. The namespaces for the log4net classes are all in log4net (e.g. log4net.Appender) but they exist in the Sitecore.Logging.dll file. Don’t get confused and add the log4net.dll file. This is not the correct library. All the namespaces and class names are the same or similar, but they will not work as expected.

Building an ADONetAppender

Out of the box, Sitecore comes with an Appender named ADONetAppender (if you have just AdoNetAppender - you have the wrong DLL). Using this Appender is just fine, but I wanted to include some more fields. Originally I had done it in a helper class that I would call before logging an error. But by creating my own Appender, I could handle it all in one place.
The first thing I did was to inherit from the built in appender:
public class AdoNetAppender : log4net.Appender.ADONetAppender {
Then I override the Append method:
protected override void Append(log4net.spi.LoggingEvent loggingEvent) {
Inside that method, I found all the information that I needed. The LoggingEvent object that’s passed contains a property named Properties that will contain all the information available to the appender:
log4net.helpers.PropertiesCollection col = loggingEvent.Properties;
Then they can be assigned as follows:
col["{PROPERTYNAME}"] = “Value”;
After all the properties are set, I simply called the base append method:
base.Append(loggingEvent);
Here is the full set of code for the custom AdoNetAppender: (note: this contains some methods that were originally part of extention methods for the Language class, but was added to this class for brevity)
    public class AdoNetAppender : log4net.Appender.ADONetAppender {
        public string connectionStringName { get; set; }

        public override void ActivateOptions() {
            this.ConnectionString = System.Configuration.ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString;
            base.ActivateOptions();
        }


        private static Language tryParseLanguage(string name) {
            Sitecore.Diagnostics.Assert.ArgumentNotNull((object)name, "name");
            Language result;
            if (tryParseLanguage(name, out result))
                return result;
            throw new ArgumentException(string.Format("Could not parse the language '{0}'. Note that a custom language name must be on the form: isoLanguageCode-isoRegionCode-customName. The language codes are two-letter ISO 639-1, and the regions codes are are two-letter ISO 3166. Also, customName must not exceed 8 characters in length. Valid example: en-US-East. For the full list of requirements, see: http://msdn2.microsoft.com/en-US/library/system.globalization.cultureandregioninfobuilder.cultureandregioninfobuilder.aspx", (object)name));
        }

        private static bool tryParseLanguage(string name, out Language result) {
            Sitecore.Diagnostics.Assert.ArgumentNotNull((object)name, "name");
            result = (Language)null;
            if (name.Equals("__Standard Values", StringComparison.OrdinalIgnoreCase) || name.Equals("__language", StringComparison.OrdinalIgnoreCase) || !LanguageManager.IsValidLanguageName(name))
                return false;
            if (LanguageManager.LanguageRegistered(name) || LanguageManager.RegisterLanguage(name)) {
                return true;
            }
            return true;
        }



        private Language getLanguage() {
            Language language1 = Sitecore.Context.Items["sc_Language"] as Language;
            if (language1 != null)
                return language1;
            string name = string.Empty;
            Sitecore.Sites.SiteContext site = Sitecore.Context.Site;
            if (site != null) {
                name = WebUtil.GetCookieValue(site.GetCookieKey("lang"), site.Language);
                Assert.IsNotNull((object)name, "languageName");
            }
            if (name.Length == 0)
                name = Settings.DefaultLanguage;

            try {
                Language language2 = tryParseLanguage(name);
                if (Sitecore.Configuration.State.Sites.IsSiteResolved)
                    Sitecore.Context.Items["sc_Language"] = (object)language2;
                return language2;
            } catch (Exception) { }
            return null;
        }


        protected override void Append(log4net.spi.LoggingEvent loggingEvent) {
            log4net.helpers.PropertiesCollection col = loggingEvent.Properties;
            if (!object.ReferenceEquals(Environment.MachineName, null)) {
                col["machine"] = Environment.MachineName;
            }

            HttpContext context = null;
            User currentUser = null;
            Item contextItem = null;
            Language language = null;

            try { context = HttpContext.Current; } catch { }
            try { currentUser = Sitecore.Context.User; } catch { }
            try { contextItem = Sitecore.Context.Item; } catch { }

            if (loggingEvent.LoggerName != "System.RuntimeType") {
                try { language = Sitecore.Context.Language; } catch { }
                if (language != null) {
                    col["Language"] = language;
                }
            }

            try {
                if (currentUser != null) {
                    col["CurrentUser"] = currentUser.DisplayName;

                    var roles = "";
                    foreach (var role in currentUser.Roles) {
                        roles += role.DisplayName + ",";
                    }
                    roles = roles.Substring(0, roles.Length - 1); // remove last comma
                    col["Roles"] = roles;
                }
            } catch { }

            if (contextItem != null) {
                col["SitecoreItemID"] = contextItem.ID.ToString();
                col["SitecoreItemName"] = contextItem.Name;
            }

            if (context != null) {
                try {
                    string ipAddress = context.Request.ServerVariables["HTTP_CLIENT_IP"];
                    if (String.IsNullOrEmpty(ipAddress)) {
                        ipAddress = context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
                    }
                    if (String.IsNullOrEmpty(ipAddress)) {
                        ipAddress = context.Request.ServerVariables["REMOTE_ADDR"];
                    }
                    if (!String.IsNullOrEmpty(ipAddress)) {
                        col["IpAddress"] = ipAddress;
                    }

                    if (!String.IsNullOrEmpty(context.Request.ServerVariables["X-FORWARDED-FOR"])) {
                        col["ForwardedIpAddress"] = context.Request.ServerVariables["X-FORWARDED-FOR"];
                    }

                    string referrer = context.Request.ServerVariables["HTTP_REFERER"];
                    col["HttpReferrer"] = referrer;

                    string currentUrl = context.Request.Url.AbsoluteUri;
                    col["HttpUrl"] = currentUrl;

                    string method = context.Request.HttpMethod;
                    col["HttpMethod"] = method;

                    if (method.ToLower() == "post") {
                        var formVariables = "";
                        if (context.Request.Form.Count > 0) {
                            foreach (string key in context.Request.Form) {
                                var value = context.Request.Form[key];
                                formVariables += "|   " + key + " : " + value.ToString() + System.Environment.NewLine;
                            }
                        }
                        col["FormVariables"] = formVariables;
                    }

                    HttpCookieCollection cookies = context.Request.Cookies;
                    string userAgent = context.Request.ServerVariables["HTTP_USER_AGENT"];
                    col["HttpUserAgent"] = userAgent;

                    string queryString = context.Request.ServerVariables["QUERY_STRING"];
                    if (!String.IsNullOrEmpty(queryString)) {
                        col["HttpQueryString"] = queryString;
                    }

                    if (cookies != null && cookies.Count > 0) {
                        var strCookies = "";
                        for (int c = 0; c < cookies.Count; c++) {
                            HttpCookie cookie = cookies.Get(c);
                            strCookies += String.Format(CultureInfo.CurrentCulture, "|  HTTP Cookie: {0} = {1}{2}", cookie.Name, cookie.Value, Environment.NewLine);
                        }
                        col["HttpCookies"] = strCookies;
                    }
                } catch { }
            }

            base.Append(loggingEvent);
        }
    }

The log4net configuration

Next I needed to add an appender into the log4net configuration section. I did this in the usual Sitecore include file. It belongs under configuration/sitecore/log4net and references the custom AdoNetAppender class I created:
<appender name="AdoNetAppender" type="Rizzi.SC.AdoLogging.Appenders.AdoNetAppender, Rizzi.SC.AdoLogging">
Underneath that we can set the log level. In this case we are only logging ERRORS or worse:
<level value="ERROR" />
Then we add the connection type. In this case I’m using an SqlConnection:
<param name="ConnectionType" value="System.Data.SqlClient.SqlConnection, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
For the connection string, we added a param named connectionStringName. This is automatically mapped to the property of the same name in the custom AdoNetAppender class. The value of this property maps to a connection string of the same name in the connection string file:
<param name="connectionStringName" value="log4net"/>
Please note that you can add any property you want to an Appender class and specify the value as a parameter in the config file.
Next is the CommandText. This is the insert statement for inserting the actual data into the table:
<param name="CommandText" value="INSERT INTO [Log]([Date],[Thread],[Level],[Logger],[Message],[Exception],[MachineName], [CurrentUser], [Roles], [SitecoreItemID], [SitecoreItemName], [Language], [IpAddress], [ForwardedIpAddress], [HttpReferrer], [HttpUrl], [HttpMethod], [FormVariables], [HttpUserAgent], [HttpQueryString], [HttpCookies]) VALUES (@log_date, @thread, @log_level, @logger, @message, @exception, @MachineName, @CurrentUser, @Roles, @SitecoreItemID, @SitecoreItemName, @Language, @IpAddress, @ForwardedIpAddress, @HttpReferrer, @HttpUrl, @HttpMethod, @FormVariables, @HttpUserAgent, @HttpQueryString, @HttpCookies)" />
The next set of parameters are mappings for the actual fields to be logged. They are all of param name=”Parameter”:
<param name="Parameter">
There are a few different ways to reference the fields. The ParameterName sub param maps to the query in the CommandText:
<param name="ParameterName" value="@log_date" />
Then you need the type. This can be any type available in the database:
<param name="DbType" value="DateTime" />
Or
<param name="DbType" value="String" />
If a size is needed for that type, you can specify that also:
<param name="Size" value="255" />
The next part is the Layout. There are a few different Layouts that you can use, but in the end I ended up using the PatternLayout for all my parameters. There are list of different values that you can have for the ConversionPattern for the PatternLayout. There is a list here: [LINK]. Please note that I had issues along the way with there not being a 1 for 1 between this list and reality. But in the end I think that that had to do with the log4net namespace issue defined above.
The ConverionPattern starts with a % variable. They all seem to be one character (regardless of what the documentation might say). For example. %t is the tread, %p is the loglevel, etc. And, yes, the letters don’t seem to have any bearing on what they do. They are case sensitive.
For the Log Level:
<param name="Layout" type="log4net.Layout.PatternLayout">
    <param name="ConversionPattern" value="%p" />
</param>
The %d that I have has a format on it:
<param name="Layout" type="log4net.Layout.PatternLayout">
    <param name="ConversionPattern" value="%d{yyyy&apos;-&apos;MM&apos;-&apos;dd HH&apos;:&apos;mm&apos;:&apos;ss&apos;.&apos;fff}" />
</param>
For our custom properties, we need to use the ConversionPattern variable %P. You specify the Property name inside of the curly braces:
<param name="ConversionPattern" value="%P{MachineName}" />
Finally, you will need to reference this appender in your appender chain. This example just adds it to the end. If you need to patch it, you can use the usual way to patch:after or patch:before:
<root>
            <appender-ref ref="AdoNetAppender" />
</root>
Here is the full configuration:
<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
        <log4net>
            <appender name="AdoNetAppender" type="Rizzi.SC.AdoLogging.Appenders.AdoNetAppender, Rizzi.SC.AdoLogging">
                <!--<appender name="AdoNetAppender" type="log4net.Appender.ADONetAppender">-->
                <!--<bufferSize value="1" />-->
                <level value="ERROR" />

                <param name="ConnectionType" value="System.Data.SqlClient.SqlConnection, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
                <param name="connectionStringName" value="log4net"/>
                <param name="CommandText" value="INSERT INTO [Log]([Date],[Thread],[Level],[Logger],[Message],[Exception],[MachineName], [CurrentUser], [Roles], [SitecoreItemID], [SitecoreItemName], [Language], [IpAddress], [ForwardedIpAddress], [HttpReferrer], [HttpUrl], [HttpMethod], [FormVariables], [HttpUserAgent], [HttpQueryString], [HttpCookies]) VALUES (@log_date, @thread, @log_level, @logger, @message, @exception, @MachineName, @CurrentUser, @Roles, @SitecoreItemID, @SitecoreItemName, @Language, @IpAddress, @ForwardedIpAddress, @HttpReferrer, @HttpUrl, @HttpMethod, @FormVariables, @HttpUserAgent, @HttpQueryString, @HttpCookies)" />
                <param name="Parameter">
                    <param name="ParameterName" value="@log_date" />
                    <param name="DbType" value="DateTime" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%d{yyyy&apos;-&apos;MM&apos;-&apos;dd HH&apos;:&apos;mm&apos;:&apos;ss&apos;.&apos;fff}" />
                    </param>
                </param>
                <param name="Parameter">
                    <param name="ParameterName" value="@thread" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="255" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%t" />
                    </param>
                </param>
                <param name="Parameter">
                    <param name="ParameterName" value="@log_level" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="50" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%p" />
                    </param>
                </param>
                <param name="Parameter">
                    <param name="ParameterName" value="@logger" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="255" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%c" />
                    </param>
                </param>
                <param name="Parameter">
                    <param name="ParameterName" value="@message" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="-1" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%m" />
                    </param>
                </param>
                <param name="Parameter">
                    <param name="ParameterName" value="@exception" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="-1" />
                    <layout type="log4net.Layout.ExceptionLayout" />
                </param>
                <param name="Parameter">
                    <param name="ParameterName" value="@MachineName" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="4000" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%P{MachineName}" />
                    </param>
                </param>
                <param name="Parameter">
                    <param name="ParameterName" value="@CurrentUser" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="255" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%P{CurrentUser}" />
                    </param>
                </param>
                <param name="Parameter">
                    <param name="ParameterName" value="@Roles" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="255" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%P{Roles}" />
                    </param>
                </param>
                <parameter>
                    <parameterName value="@SitecoreItemID" />
                    <dbType value="String" />
                    <size value="255" />
                    <layout type="log4net.Layout.PatternLayout">
                        <conversionPattern value="%P{SitecoreItemID}" />
                    </layout>
                </parameter>
                <param name="Parameter">
                    <param name="ParameterName" value="@SitecoreItemName" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="255" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%P{SitecoreItemName}" />
                    </param>
                </param>
                <param name="Parameter">
                    <param name="ParameterName" value="@Language" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="255" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%P{Language}" />
                    </param>
                </param>
                <param name="Parameter">
                    <param name="ParameterName" value="@IpAddress" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="255" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%P{IpAddress}" />
                    </param>
                </param>
                <param name="Parameter">
                    <param name="ParameterName" value="@ForwardedIpAddress" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="255" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%P{ForwardedIpAddress}" />
                    </param>
                </param>
                <param name="Parameter">
                    <param name="ParameterName" value="@HttpReferrer" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="-1" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%P{HttpReferrer}" />
                    </param>
                </param>
                <param name="Parameter">
                    <param name="ParameterName" value="@HttpUrl" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="-1" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%P{HttpUrl}" />
                    </param>
                </param>
                <param name="Parameter">
                    <param name="ParameterName" value="@HttpMethod" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="255" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%P{HttpMethod}" />
                    </param>
                </param>
                <param name="Parameter">
                    <param name="ParameterName" value="@FormVariables" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="-1" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%P{FormVariables}" />
                    </param>
                </param>
                <param name="Parameter">
                    <param name="ParameterName" value="@HttpUserAgent" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="255" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%P{HttpUserAgent}" />
                    </param>
                </param>
                <param name="Parameter">
                    <param name="ParameterName" value="@HttpQueryString" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="-1" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%P{HttpQueryString}" />
                    </param>
                </param>
                <param name="Parameter">
                    <param name="ParameterName" value="@HttpCookies" />
                    <param name="DbType" value="String" />
                    <param name="Size" value="-1" />
                    <param name="Layout" type="log4net.Layout.PatternLayout">
                        <param name="ConversionPattern" value="%P{HttpCookies}" />
                    </param>
                </param>
            </appender>
            <root>
                <appender-ref ref="AdoNetAppender" />
            </root>
        </log4net>
    </sitecore>
</configuration>

Creating the database

Here is the schema of the table:
CREATE TABLE [dbo].[Log](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [Date] [datetime] NOT NULL,
    [Thread] [varchar](255) NOT NULL,
    [Level] [varchar](255) NOT NULL,
    [Logger] [varchar](255) NOT NULL,
    [Message] [varchar](max) NOT NULL,
    [Exception] [varchar](max) NOT NULL,
    [MachineName] [varchar](4000) NOT NULL,
    [CurrentUser] [varchar](255) NOT NULL,
    [Roles] [varchar](1000) NOT NULL,
    [SitecoreItemID] [varchar](255) NOT NULL,
    [SitecoreItemName] [varchar](255) NOT NULL,
    [Language] [varchar](255) NOT NULL,
    [IpAddress] [varchar](255) NOT NULL,
    [ForwardedIpAddress] [varchar](255) NOT NULL,
    [HttpReferrer] [varchar](max) NOT NULL,
    [HttpUrl] [varchar](max) NOT NULL,
    [HttpMethod] [varchar](255) NOT NULL,
    [FormVariables] [varchar](max) NOT NULL,
    [HttpUserAgent] [varchar](255) NOT NULL,
    [HttpQueryString] [varchar](max) NOT NULL,
    [HttpCookies] [varchar](max) NOT NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

Troubleshooting and Log4net’s Log

After completing all of this, you may find yourself in a place where nothing is being logged to the table. Here are a few things you can do.
  1. Try adding <bufferSize value="1" /> to the appender configuration file. This will make sure records are written right away. For me, this didn’t seem to make a difference either way, so mine is commented at the moment.
  2. Enable Log4net’s log. This requires two configuration changes: Add the following Application Setting (not a Sitecore setting):
<add key="log4net.Internal.Debug" value="true" />
Add the following to the Web.config file underneath the Sitecore node:
<system.diagnostics>
        <trace autoflush="true">
            <listeners>
                <add    name="textWriterTraceListener"
            type="System.Diagnostics.TextWriterTraceListener"
initializeData="[PATH]\data\log4net.txt" />
            </listeners>
        </trace>
    </system.diagnostics>
Now you should get a txt file with the log4net log.

Wednesday, April 12, 2017

Sitecore Error: Could not delete candidate folder

This week I ran accross the following error:

ManagedPoolThread #10 09:55:21 INFO  Directory is being deleted by cleanup task: Directory name: E:\_Websites\CMS\cmwebsite\Data\diagnostics\configuration_history\20151211Z.215309Z, directory date: 12/11/2015 4:53:11 PM, age: 156.16:02:09.8430906 (min age: 00:30:00, max age: 30.00:00:00). Reason: Folder is older than max age 30.00:00:00
ManagedPoolThread #10 09:55:21 ERROR Could not delete candidate folder.
Exception: System.UnauthorizedAccessException
Message: Access to the path 'DataFolder.config' is denied.
Source: mscorlib
   at System.IO.Directory.DeleteHelper(String fullPath, String userPath, Boolean recursive, Boolean throwOnTopLevelDirectoryNotFound)
   at System.IO.Directory.Delete(String fullPath, String userPath, Boolean recursive, Boolean checkHost)
   at Sitecore.Tasks.FileCleaner.GetCandidateFiles(DirectoryInfo folder)

The permission all looked correct and then I came across this knowledge base article. This pointed me to check the advanced permissions on the data folder. What is needed is to set the "Delete subfolders and files" permission on the Data folder.

1. Right click the Data folder and select properties.

2. Go the Security tab and select Advanced.
 

3. Click on the IIS Application Pool user and select Edit.


4. Click on "Show advanced permissions"


5.Check the "Delete subfolders and files" permission.


6. OK your way out, waiting for permission propagation when it comes and you should be good.

If you're still not good, then also make sure the read-only flag is not on. What happens is that Sitecore will keep a copy of any configs that are changed and store them under the diagnostic folder. If the source files have the read-only flag, it will persist when the copy is made. Make sure the App_Config folder and its children don't have the read-only flag.



Wednesday, January 4, 2017

Sitecore Team Development Server - We can only call this method once error when building

I was editing my .user for my TDS.Master project when I started getting this error when building:


Error 16 The build was aborted because of an internal failure. Microsoft.Build.BuildEngine.Shared.InternalErrorException: Internal MSBuild Error: We can only call this method once at Microsoft.Build.BuildEngine.Engine.EngineBuildLoop(BuildRequest terminatingBuildRequest) at Microsoft.Build.BuildEngine.Engine.PostProjectEvaluationRequests(Project project, String[] projectFiles, String[][] targetNames, BuildPropertyGroup[] globalPropertiesPerProject, IDictionary[] targetOutputsPerProject, BuildSettings buildFlags, String[] toolVersions) 0 1 TDS.Master


The error occurred for each TDS project, not just the master one.

It occurred when adding a new Replacement node under a fairly new Build Configuration named DevCD. I undid my change, but the error remained. I switched my active Configuration to one named Debug and it compiled correctly. I added the new line to Debug, and the error showed up when building under that configuration. I removed the line from Debug and the error stayed!

<ItemGroup Condition=" '$(Configuration)' == 'DevCD' ">
  <Replacement Include="..\PATH\">
     <TargetPath>e:\PATH\</TargetPath>
     <IsFolder>True</IsFolder>
    <IsRelative>True</IsRelative>
  </Replacement>

I tried re-running the installer for TDS with the repair option, opening and closing Visual Studio, but nothing seemed to help.

What finally fixed it was to go into the TDS project settings and checked the "Edit user specific configuration (.user file) under Build. It re-wrote the .user file and everything worked again. I did a compare and the only thing that changed were line breaks and spacing - but the error went away. It doesn’t matter which Configuration is selected, since the .user file contains the settings from all.


FYI - when going back into settings, the check box will always be unchecked by default and checking it will re-create your file.

Friday, November 18, 2016

Sitecore Team Development Server 5.5

Installed the new Team Development For Sitecore 5.5 today. The new feature for auto deploying content files is something I've been needing for a long time. It cuts development time when working on CSS, Javascript and even cshtml changes by 1000%. This will allow me to see my changes much faster and has a direct impact on development time.

There are quite a few other features I'm looking forward to experiencing, especially the new Delta builds.