Skip to main content

Serialization

Serialization is a key feature of the rLog framework, enabling you to pass complex data structures as part of your log entries.

rLog automatically handles encoding this data, ensuring that it can be logged, stored, or transmitted without issues.

what you'll learn
  • How rLog handles automatic serialization
  • How to pass data as part of your log entries
  • How to customize serialization using SerializationConfig
  • Best practices for managing serialized data

Passing Data with Log Entries

When logging messages using rLog, you can pass an optional second parameter that contains any additional data you want to log.

This data should be a table of string key mappings, but the values can be anything from another table, primitive types, functions, or even a class.

Basic Example

Here's a basic example of passing additional data with a log entry:

import { rLog } from "@rbxts/rlog";
import { Players } from "@rbxts/services";

const logger = new rLog();

Players.PlayerAdded.Connect((player) => {
logger.i("Player joined", { id: player.UserId, name: player.Name });
});
Console
[INFO]: Player joined
{
data: {
id: 1,
name: "ROBLOX"
}
}

In this example, the second parameter is a table containing the Player's UserId and Name.

rLog automatically serializes this data before logging it, ensuring it’s properly encoded.

Automatic Serialization

By default, rLog uses a deep serialization strategy to ensure that all data is properly encoded, including nested objects and Roblox-specific data types.

Roblox Types

A lot of Roblox data types don't properly encode to string or JSON, especially when nested in tables.

rLog will manually encode these types, so you don't have to worry about data missing in your logs.

import { rLog } from "@rbxts/rlog";

const logger = new rLog();

logger.d("User purchase complete", {
purchase_id: "121141",
details: {
item: "Nuke",
result: Enum.ProductPurchaseDecision.PurchaseGranted,
position: new Vector3(10, 15, 20),
rotation: new CFrame(),
},
});
Console
[DEBUG]: Hello debug!
{
data: {
purchase_id: "121141",
details: {
item: "Nuke",
result: "Enum.ProductPurchaseDecision.PurchaseGranted",
position: { X: 10, Y: 15, Z: 20 },
rotation: "CFrame(0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1)",
}
}
}

Although, you can also (partially) disable this behavior with the encodeRobloxTypes setting.

import { rLog } from "@rbxts/rlog";

const logger = new rLog({ serialization: { encodeRobloxTypes: false } });
Console
[DEBUG]: Hello debug!
{
data: {
purchase_id: "121141",
details: {
item: "Nuke",
result: "<Enum>",
position: "<Vector3>",
rotation: "<CFrame>",
}
}
}

This way, ROBLOX data types are still identifiable, but you're not overloaded with their internal values.

This is especially useful if the values of the properties are less important than their existence and types.

Nested Tables

By default, rLog will deep encode nested tables; even if they have Roblox data types, or classes. But this behavior may not be desirable with larger tables.

You can disable this behavior with the deepEncodeTables setting.

warning

While this will save you on performance, keep in mind that this may cause certain nested types to not be properly encoded.

import { rLog } from "@rbxts/rlog";

const event = {
source: {
position: new Vector2(1, 1),
distance: 100,
},
target: new Vector2(2, 2),
};

const logger = new rLog({ serialization: { deepEncodeTables: false } });

logger.i("Nuke sent", event);
Console
[INFO]: Nuke sent
{
data: {
source: {
position: null,
distance: 100,
},
target: { X: 1, Y: 2 }
}
}

Normally, rLog would be able to encode the position.

But, with deepEncodeTables disabled- it's not able to properly catch it.

Classes

Instances of classes are a common issue of logging libraries. Especially if your project follows a more Object-Oriented style.

Implicit Support

By default, rLog will look for a __tostring method on class instances to try to encode them properly. This method is automatically generated by rbxts for typescript classes, with a value of the class name.

import { rLog } from "@rbxts/rlog";

class Person {
constructor(public name: string) {}
}

const logger = new rLog();

const person = new Person("Daymon");

logger.debug("Person created", { me: person });
Console
[DEBUG]: Person created
{ data: { me: "Person" } }

Custom encoding

Although, you can customize this behavior.

There are two ways you can customize the encoding process for class instances.

Overriding

The first (and easiest) way is by providing your own toString override on the class.

rbxts will link to your method instead of creating their own, if you provide one.

import { rLog } from "@rbxts/rlog";

class Person {
constructor(public name: string) {}

public toString() {
return this.name;
}
}

const logger = new rLog();

const person = new Person("Daymon");

logger.debug("Person created", { me: person });
Console
[DEBUG]: Person created
{ data: { me: "Daymon" } }
Custom Encode Methods

You can also provide a custom encodeMethod for the rLog serializer to look for.

info

In the case that your encodeMethod is not found on an instance, rLog will fall back to treating it is as a table.

import { rLog } from "@rbxts/rlog";

class Person {
constructor(public name: string) {}

public encode() {
return this.name;
}
}

const logger = new rLog({ serialization: { encodeMethod: "encode" } });

const person = new Person("Daymon");

logger.debug("Person created", { me: person });
Console
[DEBUG]: Person created
{ data: { me: "Daymon" } }

This way, you can retain existing interop with systems that depend on your current toString method, and still change the format of your classes in logs.

tip

The serializer actually expects an EncodableValue as the return value.

Meaning you can also return tables, numbers, or even booleans- if a string isn't enough for you.

Functions

A not so common use-case is functions.

By default, rLog does not encode functions in the output. But, this behavior can sometimes be desirable (e.g., if you're inspecting run-time types).

You can enable this behavior with the encodeFunctions config option.

import { rLog } from "@rbxts/rlog";

function CreatePlayer(name: string) {
return {
name: name,
eatFood: () => {
// ...
},
};
}

const logger = new rLog({ serialization: { encodeFunctions: true } });

const player = CreatePlayer("daymon");

logger.i("Player created", { player: player });
Console
[INFO]: Player created
{
data: {
player: {
name: "daymon",
eatFood: "<Function>"
}
}
}

Functions will be encoded as <Function> alongside their key name.

Self Pointers

An edge case behavior that might come up is you have a table that has a reference to itself. This could be intentional, or on accident. Normally, this would could issues during encoding procedures; as it would cause a stack overflow. Thankfully, rLog fixes that.

warning

While rLog will catch surface level self pointers, it does not catch deep self pointers.

This is done for performance reasons. If you have deeply nested self pointers, you may want to provide a custom encoding method to fix it.

import { rLog } from "@rbxts/rlog";

class Person {
constructor(
public name: string,
public parent?: Person,
) {}

public encode() {
return {
name: this.name,
parent: this.parent,
};
}
}

const logger = new rLog({ serialization: { encodeMethod: "encode" } });

const person = new Person("Daymon");

person.parent = person;

logger.debug("Person created", { me: person });
Console
[DEBUG]: Person created
{
data: {
me: {
name: "Daymon",
parent: "<PtrToSelf>"
}
}
}

rLog will convert self pointers to the string <PtrToSelf> to avoid these issues.

Best Practices

  • Provide metadata when possible: While there's definitely such a thing as cognitive overload when it comes to logs, that can usually be fixed with proper filtering. It's generally a better idea to provide metadata for logs where possible- even in small amounts. You'll be thanking yourself when it comes time to do some deep debugging.
  • Attach IDs to logs: Any sort of identifying information can be a great help when debugging issues. So you should try to make sure IDs like Player IDs, Asset IDs, etc., are included in your logs.

Summary

Let's recap what we've learned about Serialization:

  • Serialization occurs automatically for ROBLOX data types and complex data structures.
  • You can configure your serialization with SerializationConfig settings.
  • You can setup custom serialization for classes and in-house data structures.