Modular architecture for Unity 3D #3: Logger

In previous note I talked about handling additive scenes with their own modules. This time I will focus on something more universal, custom logger.

I’m a big fan of Editor Console Pro plugin with it I can organise my logs into handy, color-coded tabs (filtered groups). It also provides better search feature.

Creating filtered groups with Console Pro

Debug.Log("#Tab Name# This is our log!");

Easy enough, this line of code will create new filtered group called Tab Name. If you have more than two or few logs in your class/category you will have to type that “tab” name and if you are like me you will get annoyed and at some point you will make a typo.

So maybe we can create constant in each class that uses filtered group feature? Great idea!

Quick warning

Using filtered groups with LogWarning and LogError can be done but for me it’s useless since logs can only show up in one group. Basically warnings and errors will hide from you inside of custom filtered group.

Making life easier

const string FILTERED_LOG_FORMAT = "#Tab Name# {0}";

public void TestLog()
{
  string log = string.Format(FILTERED_LOG_FORMAT, "This is our log!");
  Debug.Log(log);
}

That’s better, but this brings us to another problem. In one of my projects I had combat module. Module was responsible for handling well… combat. It was using handful of helpers to calculate damage. I wanted all those calculations to be logged within the same filtered group. Lazy solution was to create const in each of those classes, use LogFormat or to make const variables public. I spent some time being lazy and using both solutions and I was getting more and more annoyed with this… and then I got distracted.

Making life prettier

Somewhere on YT I saw video about using Rich Text in console to make things nicer and I knew that I needed this in my life project… one quick string extension later I had this.

public static class StringExtension
{
  private const string BOLD_FORMAT = "<b>{0}</b>";
  private const string ITALIC_FORMAT = "<i>{0}</i>";
  private const string SIZE_FORMAT = "<size={1}>{0}</size>";
  private const string COLOR_FORMAT = "<color={1}>{0}</color>";
  
  public static string Bold(this string str)
  {
    return string.Format(BOLD_FORMAT, str);
  }

  public static string Italic(this string str)
  {
    return string.Format(ITALIC_FORMAT, str);
  }

  public static string Size(this string str, int size)
  {
    return string.Format(SIZE_FORMAT, str, size);
  }

  public static string Color(this string str, string color)
  {
    return string.Format(COLOR_FORMAT, str, color);
  }
} 

Using class extension means that I can use this for my GUI too. Anyway with this done I went back to my logging efforts.

Making life easier… again

With string extension done I wanted to use as much of it as possible. So I have expanded logging formats again. With formats being more complex I have added logging methods to AppManager.

private const string LOG_FORMAT = "#{0}# {1}\n{2}";

public void Log(string header, string message, string description = "")
{
  string log = string.Format(LOG_FORMAT, header, message.Bold(), description.Italic());
  Debug.Log(log);
}

Another amazing improvement, congrats huginn. Logs were nicer but problem with retyping header values still was there. I needed to deal with that.

Making life classier

Only solution for that header problem was to create separate class that will handling logs. Creating global logger was out of the question since it would require that header parameter. The only solution was to create pure C# class that would store header as a field.

public class Logger
{
  private readonly string _header;
  private const string LOG_FORMAT = "#{0}#{1}\n{2}";
  private const string LOG_ERROR_FORMAT = "{1}\n{2}";
  
  public Logger(string header)
  {
    this._header = header;
  }

  public void Log(string message, string description = "")
  {
    string log = string.Format(LOG_FORMAT, this._header, message.Bold(), description.Italic());
    Debug.Log(log);
  }

  public void LogError(string message, string description = "")
  {
    string log = string.Format(LOG_ERROR_FORMAT, message.Bold(), description.Italic());
    Debug.Log(log);
  }
}

It’s not full implementation of the logger class. This class is so easy to implement that I want to focus on the idea more than showcasing full implementation. Of course LogWarning is missing, it will be using same log format as LogError.

One quality of life thing that I added later were overloads of the methods with extra param of context. I guess I should quickly explain what this means. Debug.Log accepts one more parameter, GameObject. Thanks to this additional parameter clicking log in editor will highlight object in scene hierarchy.

My journey was nearly done. I would pass reference to combat module’s logger down to helper classes during initialisation of the module and… kablam everything was working perfectly and I needed to set header value only once. There was only one beast to slay before I could move logger task to Done column on my kanban board.

Making life configurable

At this point all of the combat related logs were in the same group but what if I wanted to control how detailed my logs were? Time to use config file!

I will go into more detail about config file implementation in another note. In this note I will use with simplified/pseudocode-ish version.

First thing I need was enum that would define log levels.

public enum LoggerLevel
{
  Off = 0,
  Simple = 1,
  Detailed = 2,
}

Only thing left to do was to modify Logger and make it respect log levels.

public class Logger
{
  (...)
  private readonly LoggerLevel _loggerLevel;

  public Logger(LoggerLevel loggerLevel, string header)
  {
    this._loggerLevel = loggerLevel;
    this._header = header;
  }

  public void Log(LoggerLevel loggerLevel, string message, string description = "")
  {
    if ((int) loggerLevel <= (int) this._loggerLevel)
    {
      string log = string.Format(LOG_FORMAT, this._header, message.Bold(), description.Italic());
      Debug.Log(log);
    }
  } 
}

Am I done?

Yes… I mean… yes… for now… It’s complicated… Truth be told before I started writing this note I was happy with my Logger. Somewhere around classier life I realised that I’m missing one feature that would be nice to have. Saving logs into file. Yes I’m fully aware that Unity is doing that already. At the same time I had to explain how to find those files too many times and every time this was annoying experience.

Another reason to add that functionality is quality of life. This way I can create log file that is easy easier to read and can be used (with some extra effort) with some kind of custom debugger (player). Custom player/debugger would treat log file as recording of everything that have happened and use that info to visualise what happened. I can imagine that this would help with the bug reproduction process. But this is idea for another day…