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.
- 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 });
});
{
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(),
},
});
{
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 } });
{
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.
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);
{
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 });
{ 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 });
{ data: { me: "Daymon" } }
Custom Encode Methods
You can also provide a custom encodeMethod
for the rLog serializer to look for.
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 });
{ 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.
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 });
{
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.
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 });
{
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.