One of the main problems with working on any sort of project, whether it is a game or not, is how to detect and report bugs to the developers in an easy/straightforward manner. In a previous post I talked a bit about various options out there and proposed using a database. Using a database for this sort of work has a number of advantages (some of which I mentioned previously and some that I didn't):
-
It's quick and designed specifically to store information
-
It is easy to search through and gather information from
-
It's easy to setup and get running
So with that in mind, I've decided to use this approach within the Lunar Engine. I also decided that I was going to use MySQL as the backend portion. And while I'm not going to go over setting it up (although there is a good tutorial here), I will go over the code and database itself:
The Table
The table that I'm using for saving the errors is rather straightforward and used the following statement to set it up
CREATE TABLE errors (
`ErrorID` int(10) unsigned NOT NULL auto_increment,
`ErrorText` varchar(5000) NOT NULL,
`ErrorType` int(10) unsigned NOT NULL,
`File` varchar(1024) NOT NULL,
`Line` int(10) unsigned NOT NULL,
`StartTime` datetime NOT NULL,
`Method` varchar(1024) NOT NULL,
PRIMARY KEY (`ErrorID`)
)
This allows us to store information about the error including the error message, error type (general, lost input device, etc.), file it occurred in, line it occurred on, time that the program was started, and the method in which the error occurred. There are a few other items that we could save, but this will do for our purposes. The next step is setting up the code:
The Code
using System;
using System.IO;
namespace LunarEngine
{
public class ErrorLog
{
public ErrorLog()
{
#if DEBUG
try
{
string ConnectionString = "DRIVER={MySQL ODBC 3.51 Driver};" +
"SERVER=localhost;" + //The server location should go here
"DATABASE=errorlog;" + //The database name, in this case ErrorLog
"UID=user;" + //Whatever you've setup as the user name for the database
"PASSWORD=password;"+ //Whatever you've setup as the password for the database
"OPTION=3";
Connection = new System.Data.Odbc.OdbcConnection(ConnectionString);
Connection.Open();
Run = DateTime.Now;
}
catch (Exception a)
{
throw a;
}
#else
File = new FileStream("./Logs/Errors.txt", FileMode.CreateNew,FileAccess.Write);
Writer = new StreamWriter(File);
#endif
}
public void Write(string Message, ErrorType ErrorType)
{
try
{
#if DEBUG
System.Diagnostics.StackFrame CallStack = new System.Diagnostics.StackFrame(1, true);
string File = CallStack.GetFileName().Replace("\\","\\\\");
string Method = CallStack.GetMethod().Name;
int Line = CallStack.GetFileLineNumber();
System.Data.Odbc.OdbcCommand Command;
string CommandString = "insert into errors(ErrorText,ErrorType,File,Line,StartTime,Method)"+
"values (\"" +
Message + "\"," + ((int)ErrorType).ToString() + ",\"" +
File + "\"," + Line.ToString() + ",\""+
Run.ToString("yyyy-MM-dd hh:mm:ss")+"\",\""+Method+"\");";
Command = new System.Data.Odbc.OdbcCommand(CommandString, Connection);
int LinesInserted = Command.ExecuteNonQuery();
#else
Writer.Write(((int)ErrorType).ToString()+" "+Message);
#endif
}
catch (Exception a)
{
throw a;
}
}
public void Cleanup()
{
#if DEBUG
if(Connection!=null)
{
if(Connection.State!=System.Data.ConnectionState.Closed)
{
Connection.Close();
}
}
#else
Writer.Close();
File.Close();
#endif
}
private System.Data.Odbc.OdbcConnection Connection;
private FileStream File;
private StreamWriter Writer;
private DateTime Run;
public enum ErrorType
{
General = 1
};
}
}
Final Word
There are a couple of areas where the code might confuse you:
Why am I not passing in the file, method, and line information to the function?
Well in C# there is no __FILE__ or __LINE__ constants. The only way to really get that information is to use the StackFrame class. It's a bit awkward but it will let you know what you need. Also note though that it will not work in release mode as the debug information that it uses isn't available.
Why are you using this system only in debug builds?
The reasons for using a more traditional file system to record this information in release is two fold. First, we would have to setup the ODBC driver on the end user's machine. The second issue is that it could be just a bit too slow. While it is fast enough when the database is on the same machine as the application, the error message would have to be sent to the server, a response sent back, etc. if it were left in during release. To be honest that isn't really something we want to deal with. There are ways we could address that issue, run the error logging system in its own thread for example, but we should save those threads for items that would enhance our game.
This approach as you can see is easy to setup though and fast enough for our purposes when it comes to debugging the system. There's another advantage that I haven't talked much about yet, our ability to search through the information. We can easily setup a website to use the information to present it in a readable manner that can allow us to filter out the noise and get to the info we want. I will say that I've talked enough though and I'll go over this further in a future post.