promotional bannermobile promotional banner

LibLog-1.0

A configurable logging library.

LibLog-1.0

LibLog-1.0 is a logging library for World of Warcraft. It aims to be easy to use with a simple API. LibLog-1.0's biggest feature is that its built for structured logging.

There are several related addons available which can capture the output of LibLog-1.0 and visualize it to an in-game window, or saved variables.

MyAddon:LogInfo("My current health is {health}, and I have {mana} mana", UnitHealth("player"), UnitPower("player"))

LibLog-1.0 uses a lite¹ version of Message Templates, which is a DSL that standardizes formatted strings with named parameters. Instead of formatting variables directly into the text, LibLog-1.0 captures the value associated with the value.

The above example will record two properties (health, and mana) in the log object, when inspecting the log object, they appear in a properties table, alongside the timestamp, message, and log level.

{
  -- <other fields omitted>
  properties = {
    health = 50,
    mana = 100
  }
}

Note that even though LibLog-1.0 is built to work with structured logs, it will first and foremost still print human-readable text into the chat window. When no additional sink addons are installed, that's all it does. It still offers the full suite of features, they will just be mostly transparant, which is how 99.9% of users will experience it.

¹ Lite because it doesn't support the more advanced features such as @, $, and :000.

Structured logging.. but why?

Traditional (unstructured) logging simply writes plain text messages that can contain variable data.

print("My name is", UnitName("player"), "on realm", UnitRealm("player"))
-- My name is Arthas on realm Frostmourne

Whilst this is perfectly readable, its difficult to parse and extract data from. For example, if you prefix your logs with the name of the player that performed the action, it'd be difficult to reliably filter those logs to a specific player.

Thrall: UNIT_SPELLCAST_SUCCEEDED Bloodlust
Khadgar: UNIT_SPELLCAST_SUCCEEDED Blink
Thrall: UNIT_SPELLCAST_FAILED Lightning Bolt
Khadgar: UNIT_SPELLCAST_SUCCEEDED Fire Blast

Try determining who cast Bloodlust, who failed a cast, which player cast the most abilities, etc. especially if the number of players increases to 20, and the amount of logs increases to the duration of a typical raid fight.

Structured logging solves this by capturing and organizing data into consistent fields. With structured logging, each of the previous logs will contain the same properties.

{
  -- <other fields omitted>
  properties = {
    name = "Thrall",
    eventName = "UNIT_SPELLCAST_SUCCEEDED",
    spellName = "Bloodlust"
  }
},
{
  -- <other fields omitted>
  properties = {
    name = "Khadgar",
    eventName = "UNIT_SPELLCAST_SUCCEEDED",
    spellName = "Blink"
  }
}

This makes it trivial to query on this data and answer the above questions. It also opens the way for more debuggable addons by visualizing this data.

Features

  • Simple logging API with well-known levels such as DEBUG, INFO, WARNING, ERROR, and FATAL
  • Configurable minimum logging levels, either globally, or per-addon
  • Support for custom sinks, to capture and process logs in real-time
  • And more…

Getting started

Like many other libraries, LibLog-1.0 must be embedded within your addon. When using AceAddon-3.0, this can be done automatically, otherwise, you can do so using LibStub.

-- Option 1, use AceAddon-3.0
MyAddon = LibStub("AceAddon-3.0"):NewAddon("MyAddon", "LibLog-1.0")

-- Option 2, manually use LibStub
-- NOTE: LibLog-1.0 works best when your addon contains a 'name' property. If you use AceAddon-3.0, this is set automatically.
-- When manually embedding using LibStub, it's highly recommended to add a 'name' property to your addon object
MyAddon = {}
MyAddon.name = "MyAddon"
LibStub("LibLog-1.0"):Embed(MyAddon)

MyAddon:LogInfo("Hello {name}!", MyAddon.name)
-- Hello MyAddon!

Log levels

LibLog-1.0 implements six severity levels, combined with a minimum level to process log objects.

Level Method When to use
6 LogFatal A critical or otherwise unrecoverable error that must halt execution.
5 LogError A high-severity logic issue that leaves functionality unavailable or expections broken.
4 LogWarning User-error, or other non-breaking issues.
3 LogInfo General status updates and runtime milestones.
2 LogDebug Code paths and state changes that are useful when determining how something happened.
1 LogVerbose High-frequency, noisy data that is rarely enabled outside of debugging.

By default, the minimum log level is set to Info, meaning Info and higher levels are processed.

Configuration

You can override the minimum log level for your addon by using the following functions:

MyAddon:SetLogLvel(LibLog.LogLevel.VERBOSE)
MyAddon:GetLogLevel()

It's likely also desirable to initialize the logger upon startup, as your configured minimum log level is not persisted by the library — that responsibility is for your addon itself. LibLog-1.0 does offer functionality to mostly do it for you.

function MyAddon:OnInitialize()
  MyAddon.db = LibStub("AceDB-3.0"):New("MyAddonDB")

  -- Simply pass in your saved variables table
  MyAddon:SetLogLevelFromConfigTable(MyAddon.db.global)
end

If using AceConfig-3.0, you can integrate a dropdown to set the log level for your addon directly:

{
  logLevel = MyAddon:GetLogLevelOptionObject(MyAddon.db.global)
}
{
  logLevel = Mixin(MyAddon:GetLogLevelOptionObject(MyAddon.db.global), {
    order = 10
  })
}

If not using AceConfig-3.0, all required properties to build a dropdown youself are publicly available: LibLog.LogLevel, LibLog.CONFIG_KEY, and of course MyAddon:SetLogLevel and MyAddon:GetLogLevel.

Advanced configuration

Callback functions

Sometimes a log may require complex logic to be performed which is only used for that log, if then that log is filtered out because of severity level, all that computational power has gone to waste.

All log methods support a callback function which is executed on-demand, and can be used to create logs that need to perform heavy calculations. This ensures that during normal gameplay performance is not affected by these logs.

MyAddon:LogVerbose("Currently in a raid with {members}", function()
  -- this code will only run if the log level is set to VERBOSE
  local result = {}

  for i = 1, GetNumGroupMembers() do
    table.insert(result, UnitName("raid" .. i))
  end

  return result
end)

Alternatively, you can use IsLogLevelEnabled to check whether logging should occur.

if MyAddon:IsLogLevelEnabled(LibLog.LogLevel.DEBUG) then
  local members = {}

  for i = 1, GetNumGroupMembers() do
    table.insert(members, UnitName("raid" .. i))
  end

  MyAddon:LogDebug("Currently in a raid with {members}", members)
end

Halting execution

The fatal log level is a bit special, it behaves just like a regular log would, however it will also show up in BugSack as a captured error. This ensures that execution is halted, just like a regular error(...) would.

All log functions return an unknown value, for the sole purpose of being able to immediately return after the log. Since LuaLS or the likes doesn't know that LogFatal does not return, it may be desirable to return MyAddon:LogFatal(...).

Additional properties

You can also add extra properties to a log object, these are only included in the properties table, and not visible within the message itself. It's also possible to push a callback function as property, these will be evaluated on-demand with every log, allowing you to capture up-to-date information.

MyAddon:PushLogProperty("extra", 41)
MyAddon:PushLogProperty("anotherProperty", function()
  return UnitHealth("player")
end)

After pushing a property, all logs your addon produces will contain the value of that property, until popped.

MyAddon:PopLogProperty("extra", "anotherProperty")

You can also use closures to automatically manage pushing and popping properties:

MyAddon:WithLogContext({ extra = 41, anotherProperty = function() return UnitHealth("player") end }, function()
  MyAddon:LogInfo("This log will have additional properties")

  -- <logic>

  MyAddon:LogVerbose("This log will still have additional properties")
end)

Custom sinks

And finally, the primary reason LibLog-1.0 exists, custom sinks to process log objects.

To register a custom sink, use the following function:

--- @param message LibLog-1.0.LogMessage
local function OnLogMessageReceived(message)
  print(message.message)
end

LibLog:RegisterSink("MyLogSink", OnLogMessageReceived)

You can also pass in an object that contains a OnLogMessageReceived function:

--- @class MySink : LibLog-1.0.Sink
local MySink = {}

--- @param message LibLog-1.0.LogMessage
function MySink:OnLogMessageReceived(message)
  print(message.message)
end

LibLog:RegisterSink("MyLogSink", MySink)

The LibLog-1.0.LogMessage message object contains all relevant information for the log message, including the value of each individial property.

{
  message = "My character name is Arthas on realm Frostmourne",
  template = "My character name is {charName} on realm {realmName}",
  addon = "MyAddon",
  level = 4,
  time = 1771420921,
  sequenceId = 1,
  properties = {
    charName = "Arthas",
    realmName = "Frostmourne"
  }
}

You can also enable or disable registered sinks:

LibLog:EnableSink("MyLogSink")
LibLog:DisableSink("MyLogSink")

Available sinks

The LibLog-1.0 Team

profile avatar
Owner
  • 3
    Followers
  • 6
    Projects
  • 462.4K
    Downloads

More from SnakyboView all