Lunar Engine Error Logging System

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.

kick it on DotNetKicks.com   Shout it
Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkListEmail

Posted by: James Craig
Posted on: 2/26/2008 at 4:19 PM
Tags: , ,
Categories: C# | Database | Game Programming | MySQL
Post Information: Permalink | Comments (0) | Post RSSRSS comment feed

Using a Database for Logging Events

One of the things I've been trying to do with my new engine/game is improve on The vast majority of games use a system for capturing errors/information like the one found here. While it might not be XML based, the fact of the matter is that it usually goes to a file on the users computer. In order for the developer to get the information the user must find the file, upload it, etc. This usually never happens, even in beta testing.

There are ways to automate this a bit by having an application that automatically uploads the file to us but we still have an issue. Specifically what if the program crashes, they start it back up (thus overwriting the error log), and then send us the error log sans error. To fight this we need either to continue to append to the file, thus creating potentially a huge file that would take forever to parse not to mention upload, or we create a new file each time the program is run (good luck finding the one with the error).

So how can we deal with these issues in a manner that wont cause people to want to beat us with sticks? Well one that I've been toying with is using a database. A database can hold numerous entries, is easily searchable, and can be centralized on a server instead of on the individual user's machine. So lets take a look at how we might do this.

Setup The Database

I'm not talking about installing the database or get into a discussion of whether you should use MySQL, SQL Server, etc. So at this point I'm just going to assume that you've made your decision, installed it, and configured it correctly on your server. However we still need to setup the tables. For this example we're only going to use a simple example using MySQL and C#. Assume that we setup a database/table using the following SQL:

create database ErrorLog;

use ErrorLog;

create table Errors (ErrorID int auto_increment not null, message char(5000), ErrorType int, File char(1024), Line int, primary key(ErrorID));

As you can see, we should have at this point a simple table that has an ID number, a spot for a message, error type, the file that the error came from, and the line number.

Setup The Code 

Now in order to insert the information we can use a function like this:

public static void Error(string message,int ErrorType,string File,int Line)
{


System.Data.Odbc.OdbcConnection Connection;
string ConnectionString = "DRIVER={MySQL ODBC 3.51 Driver};" +
               "SERVER=ServerName;" +       //The server location should go here
               "DATABASE=ErrorLog;" +         //The database name, in this case ErrorLog
               "UID=UserID;" +                     //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();
System.Data.Odbc.OdbcCommand Command;
string CommandString = "insert into Errors(message,ErrorType,File,Line) values (" + message + "," + ErrorType + "," + File + "," + Line + ");";
Command = new System.Data.Odbc.OdbcCommand(CommandString, Connection);
int LinesInserted = Command.ExecuteNonQuery();

Connection.Close();


}

And voilà, we have a central database that contains all errors that occur, tells us where in the code they occur, etc. with the user not having to lift a finger.

Now there are some downsides to logging your events this way. First and foremost is that it is potentially rather slow. You have to wait for a message to be sent from the user to the database and back. So sending a couple of messages each loop will likely make your application crawl. Instead we could save the information onto the users machine in an XML file and only send the data when the application is closing. Or potentially we could only use this method during development and testing of the application and remove it for a more traditional system when it hits production.

Another issue is the fact that the person has to be connected to the Internet in order for this to work. However for games that are either strictly multiplayer (World of Warcraft, Shadowrun, etc.) or use a system like Steam might not be hindered by this aspect.

Lastly we have to worry about privacy issues. Any time we send information from the user's machine, we have to tell them and let them have the ability to opt out if they wish. If we do not, we risk angering quite a few people. So basically make sure they know about it, can opt out, make it anonymous, etc. and you shouldn't have many issues here.

So while this might not be the perfect solution for every game, it definitely is something to consider for some. The key is to experiment and find what works for you and your situation.

kick it on DotNetKicks.com   Shout it
Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkListEmail

Posted by: James Craig
Posted on: 2/17/2008 at 1:08 PM
Tags: , , , ,
Categories: Game Programming | Database | C# | MySQL
Post Information: Permalink | Comments (0) | Post RSSRSS comment feed