#region License, Terms and Author(s) // // ELMAH - Error Logging Modules and Handlers for ASP.NET // Copyright (c) 2004-9 Atif Aziz. All rights reserved. // // Author(s): // // James Driscoll, mailto:jamesdriscoll@btinternet.com // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // #endregion // All code in this file requires .NET Framework 2.0 or later. #if !NET_1_1 && !NET_1_0 [assembly: Elmah.Scc("$Id$")] namespace Elmah { #region Imports using System; using System.Data; using System.Globalization; using System.IO; using System.Text.RegularExpressions; using VistaDB; using VistaDB.DDA; using VistaDB.Provider; using IDictionary = System.Collections.IDictionary; using IList = System.Collections.IList; #endregion /// /// An implementation that uses VistaDB as its backing store. /// public class VistaDBErrorLog : ErrorLog { private readonly string _connectionString; private readonly string _databasePath; // TODO - don't think we have to limit strings in VistaDB, so decide if we really need this // Is it better to keep it for consistency, or better to exploit the full potential of the database?? private const int _maxAppNameLength = 60; /// /// Initializes a new instance of the class /// using a dictionary of configured settings. /// public VistaDBErrorLog(IDictionary config) { if (config == null) throw new ArgumentNullException("config"); _connectionString = ConnectionStringHelper.GetConnectionString(config); // // If there is no connection string to use then throw an // exception to abort construction. // if (_connectionString.Length == 0) throw new ApplicationException("Connection string is missing for the VistaDB error log."); _databasePath = ConnectionStringHelper.GetDataSourceFilePath(_connectionString); InitializeDatabase(); string appName = Mask.NullString((string)config["applicationName"]); if (appName.Length > _maxAppNameLength) { throw new ApplicationException(string.Format( "Application name is too long. Maximum length allowed is {0} characters.", _maxAppNameLength.ToString("N0"))); } ApplicationName = appName; } /// /// Initializes a new instance of the class /// to use a specific connection string for connecting to the database. /// public VistaDBErrorLog(string connectionString) { if (connectionString == null) throw new ArgumentNullException("connectionString"); if (connectionString.Length == 0) throw new ArgumentException(null, "connectionString"); _connectionString = connectionString; _databasePath = ConnectionStringHelper.GetDataSourceFilePath(_connectionString); InitializeDatabase(); } /// /// Gets the name of this error log implementation. /// public override string Name { get { return "VistaDB Error Log"; } } /// /// Gets the connection string used by the log to connect to the database. /// public virtual string ConnectionString { get { return _connectionString; } } /// /// Logs an error to the database. /// /// /// Use the stored procedure called by this implementation to set a /// policy on how long errors are kept in the log. The default /// implementation stores all errors for an indefinite time. /// public override string Log(Error error) { if (error == null) throw new ArgumentNullException("error"); string errorXml = ErrorXml.EncodeString(error); using (VistaDBConnection connection = new VistaDBConnection(this.ConnectionString)) using (VistaDBCommand command = connection.CreateCommand()) { connection.Open(); command.CommandText = @"INSERT INTO ELMAH_Error (Application, Host, Type, Source, Message, [User], AllXml, StatusCode, TimeUtc) VALUES (@Application, @Host, @Type, @Source, @Message, @User, @AllXml, @StatusCode, @TimeUtc); SELECT @@IDENTITY"; command.CommandType = CommandType.Text; VistaDBParameterCollection parameters = command.Parameters; parameters.Add("@Application", VistaDBType.NVarChar, _maxAppNameLength).Value = ApplicationName; parameters.Add("@Host", VistaDBType.NVarChar, 30).Value = error.HostName; parameters.Add("@Type", VistaDBType.NVarChar, 100).Value = error.Type; parameters.Add("@Source", VistaDBType.NVarChar, 60).Value = error.Source; parameters.Add("@Message", VistaDBType.NVarChar, 500).Value = error.Message; parameters.Add("@User", VistaDBType.NVarChar, 50).Value = error.User; parameters.Add("@AllXml", VistaDBType.NText).Value = errorXml; parameters.Add("@StatusCode", VistaDBType.Int).Value = error.StatusCode; parameters.Add("@TimeUtc", VistaDBType.DateTime).Value = error.Time.ToUniversalTime(); return Convert.ToString(command.ExecuteScalar(), CultureInfo.InvariantCulture); } } /// /// Returns a page of errors from the databse in descending order /// of logged time. /// public override int GetErrors(int pageIndex, int pageSize, IList errorEntryList) { if (pageIndex < 0) throw new ArgumentOutOfRangeException("pageIndex", pageIndex, null); if (pageSize < 0) throw new ArgumentOutOfRangeException("pageSize", pageSize, null); VistaDBConnectionStringBuilder builder = new VistaDBConnectionStringBuilder(_connectionString); // Use the VistaDB Direct Data Access objects IVistaDBDDA ddaObjects = VistaDBEngine.Connections.OpenDDA(); // Create a connection object to a VistaDB database IVistaDBDatabase vistaDB = ddaObjects.OpenDatabase(_databasePath, builder.OpenMode, builder.Password); // Open the table IVistaDBTable elmahTable = vistaDB.OpenTable("ELMAH_Error", false, true); elmahTable.ActiveIndex = "IX_ELMAH_Error_App_Time_Id"; if (errorEntryList != null) { if (!elmahTable.EndOfTable) { // move to the correct record elmahTable.First(); elmahTable.MoveBy(pageIndex * pageSize); int rowsProcessed = 0; // Traverse the table to get the records we want while (!elmahTable.EndOfTable && rowsProcessed < pageSize) { rowsProcessed++; string id = Convert.ToString(elmahTable.Get("ErrorId").Value, CultureInfo.InvariantCulture); Error error = new Error(); error.ApplicationName = (string)elmahTable.Get("Application").Value; error.HostName = (string)elmahTable.Get("Host").Value; error.Type = (string)elmahTable.Get("Type").Value; error.Source = (string)elmahTable.Get("Source").Value; error.Message = (string)elmahTable.Get("Message").Value; error.User = (string)elmahTable.Get("User").Value; error.StatusCode = (int)elmahTable.Get("StatusCode").Value; error.Time = ((DateTime)elmahTable.Get("TimeUtc").Value).ToLocalTime(); errorEntryList.Add(new ErrorLogEntry(this, id, error)); // move to the next record elmahTable.Next(); } } } return Convert.ToInt32(elmahTable.RowCount); } /// /// Returns the specified error from the database, or null /// if it does not exist. /// public override ErrorLogEntry GetError(string id) { if (id == null) throw new ArgumentNullException("id"); if (id.Length == 0) throw new ArgumentException(null, "id"); int errorId; try { errorId = int.Parse(id, CultureInfo.InvariantCulture); } catch (FormatException e) { throw new ArgumentException(e.Message, "id", e); } catch (OverflowException e) { throw new ArgumentException(e.Message, "id", e); } string errorXml; using (VistaDBConnection connection = new VistaDBConnection(this.ConnectionString)) using (VistaDBCommand command = connection.CreateCommand()) { command.CommandText = @"SELECT AllXml FROM ELMAH_Error WHERE ErrorId = @ErrorId"; command.CommandType = CommandType.Text; VistaDBParameterCollection parameters = command.Parameters; parameters.Add("@ErrorId", VistaDBType.Int).Value = errorId; connection.Open(); // NB this has been deliberately done like this as command.ExecuteScalar // is not exhibiting the expected behaviour in VistaDB at the moment using (VistaDBDataReader dr = command.ExecuteReader()) { if (dr.Read()) errorXml = dr[0] as string; else errorXml = null; } } if (errorXml == null) return null; Error error = ErrorXml.DecodeString(errorXml); return new ErrorLogEntry(this, id, error); } private static string EscapeApostrophes(string text) { return text.Replace("'", "''"); } private static readonly object _lock = new object(); private void InitializeDatabase() { string connectionString = ConnectionString; Debug.AssertStringNotEmpty(connectionString); if (File.Exists(_databasePath)) return; // // Make sure that we don't have multiple threads all trying to create the database // lock (_lock) { // // Just double check that no other thread has created the database while // we were waiting for the lock // if (File.Exists(_databasePath)) return; VistaDBConnectionStringBuilder builder = new VistaDBConnectionStringBuilder(connectionString); using (VistaDBConnection connection = new VistaDBConnection()) using (VistaDBCommand command = connection.CreateCommand()) { string passwordClause = string.Empty; if (!string.IsNullOrEmpty(builder.Password)) passwordClause = " PASSWORD '" + EscapeApostrophes(builder.Password) + "',"; // create the database using the webserver's default locale command.CommandText = "CREATE DATABASE '" + EscapeApostrophes(_databasePath) + "'" + passwordClause + ", PAGE SIZE 1, CASE SENSITIVE FALSE;"; command.ExecuteNonQuery(); const string ddlScript = @" CREATE TABLE [ELMAH_Error] ( [ErrorId] INT NOT NULL, [Application] NVARCHAR (60) NOT NULL, [Host] NVARCHAR (50) NOT NULL, [Type] NVARCHAR (100) NOT NULL, [Source] NVARCHAR (60) NOT NULL, [Message] NVARCHAR (500) NOT NULL, [User] NVARCHAR (50) NOT NULL, [StatusCode] INT NOT NULL, [TimeUtc] DATETIME NOT NULL, [AllXml] NTEXT NOT NULL, CONSTRAINT [PK_ELMAH_Error] PRIMARY KEY ([ErrorId]) ) GO ALTER TABLE [ELMAH_Error] ALTER COLUMN [ErrorId] INT NOT NULL IDENTITY (1, 1) GO CREATE INDEX [IX_ELMAH_Error_App_Time_Id] ON [ELMAH_Error] ([TimeUtc] DESC, [ErrorId] DESC)"; foreach (string batch in ScriptToBatches(ddlScript)) { command.CommandText = batch; command.ExecuteNonQuery(); } } } } private static string[] ScriptToBatches(string script) { return Regex.Split(script, @"^ \s* GO \s* $\n?", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.IgnorePatternWhitespace); } } } #endif //!NET_1_1 && !NET_1_0