LogService: A Way To Log Your Errors And Prints

Module Link
Click Me For Module

Uncopylocked Place
Click For Place


How To Set Up The Module
Step 1: Put the module in ReplicatedStorage
Step1

Step 2: Require the module through a client or server script

local LogService = require(game.ReplicatedStorage.LogService)

Step 3: Use it by returning a function (exampled below)

-- script.Name is not needed but can be added for extra log details
game.Players.PlayerAdded:Connect(function(Player)
	LogService("Print", Player.Name .. " has joined the game!", script.Name)
end)

Use Types
There are 3 different use types, Print, Warn, & Error.
Each of these will print your log in a different way, which should be decided based off your log need.

Print: Usually used for basic logging, such as information being printed, or certain values being understood inside your game.
Print

Error: this should be self explanatory, but this type will return an error with your message data you provide.

Warn: To be honest, don’t really know what people would use this for, but I decided to make it a thing.


7 Likes

Like what’s the point???
You can recreate these functions with one line of code? You don’t need emojis and things like [WARNING] because you can identify it by the text colour(unless you are colourblind).

warn(" A player has joined the game, warned from script: "..script.Name)

same for print:

print("A player has joined the game, printed from script: "..script.Name)

and for error:

error("A player has joined the game, errored from script: ".. script.Name, 2)

You haven’t even explained why it’s better than functions I provided above. So I have no idea why would anyone use this?

People can probably write the same module in 10-20 minutes, and probably make it better than what you made now(no offence).

So idk, some people might use it, just not me.

8 Likes

what’s this for?

print('📜 [LOG] Hello World! | Logged From Script {script.Name}')
2 Likes

Was asked by someone in my community to open source it. I did not state anywhere in the post that its supposed to be better, its simply just for aesthetic and if people find it helpful they can use it.

4 Likes

Here’s something quick I made that supports vargs, and outputs reference to the script and automatically gets the script reference if not provided

type LogTypes = "Error" | "Warn" | "Print"

local Types = {
	Error = {
		Format = "🛑 [ERROR]",
		Function = function(... : any?)
			local result = "" do
				local Arguments = {...}
				local Size = #Arguments
				
				for i = 1, Size-1 do
					result ..= tostring(Arguments[i]).." "
				end
				result ..= tostring(Arguments[Size])
			end
			
			error(result)
		end,
	},
	Warn = {
		Format = "⚠️ [WARNING]",
		Function = warn,
	},
	Print = {
		Format = "📃 [LOG]",
		Function = print
	}
}

return function(ScriptName : string?, Type : LogTypes, ...)
	local LogType = Types[Type] do
		if not LogType then
			error(`{Types.Error.Format} ([ {script:GetFullName()} ] | {Type} is not a valid LogType`)
		end
	end
	
	local Orgin = ScriptName do
		if not Orgin then
			local TempScript = rawget(getfenv(2), "script")
			
			Orgin = typeof(TempScript) == "Instance" and TempScript:IsA("LuaSourceContainer") and TempScript or "Unknown"
		end
	end
	
	LogType.Function(LogType.Format.."[", Orgin, "] |", ...)
end
7 Likes

Oh woah! nice job on this, definitely going to be checking this out

2 Likes

getfenv will de-optimize the environment. You should look into debug.info for this use case.

4 Likes

Wait, if this is LogService, what about LogService?

4 Likes

what’s the point of this even existing bro

2 Likes

here’s a more simplified version

export type logType = "print" | "error" | "warn"
export type warning = "🛑 [ERROR]" | "⚠️ [WARNING]" | "📃 [LOG]"

local types = {
	error = "🛑 [ERROR]",
	warn = "⚠️ [WARNING]",
	print = "📃 [LOG]"
} :: { [string]: warning }

local functions = {
	error = error :: any,
	warn = warn :: any,
	print = print :: any,
} :: { [string]: (...string) -> () }

local logFormat = "%s "

return function<T...>(type: logType, ...: T...)
	local fn = functions[type]
	local display = types[type]
	
	local source = debug.info(2, "s")
	local sourceScript = string.match(source, "([^%.]+)$")
	
	if type == "error" then
		local args = { ... }
		local baseString = ""
		
		for _, arg in args do
			baseString ..= string.format(logFormat, arg)
		end
		
		
		fn(`{display} [{sourceScript}] | {baseString}`)
		return
	end
	
	fn(`{display} [{sourceScript}] |`, ...)
end
1 Like

Today I learned this exists.

I made a manaual logger system for a game to help identify more complex state errors, but this also helps for the general errors made during testing.

3 Likes

I have to use getfenv regardless to retrieve the calling functions environment to retrieve the calling script

1 Like

You can do that with debug.info

1 Like

Incorrect.

What you’re likely thinking of is likely the source identifier (debug.info(function() end, "s")) which returns the source that the function was defined in (it returns [C] when a C closure is passed)

What I am doing is attempting to retrieve the function’s environment to get the script instance. I have the type/IsA check in off chance the calling script overwrote the script in the function’s environment.

Currently there is no other way to retrieve the functions environment other than using getfenv

1 Like

You can literally just use debug.info(fn, "s"), and it will achieve the same result. Get the last component of the datamodel path that is returned, and you have the script name of the caller. Using getfenv on the caller will de-optimize its environment, no logging module is worth that trade-off. debug.info should cover almost all use cases.

1 Like

Incorrect.
By me retrieving the original script Instance and printing it out I am able to then go to the output and click on the script which will select the Instance

debug.info “s” returns a string which does NOT allow me to click on it to open the calling scripts document.

I fail to see what you’re trying to prove. Additionally if you care that much about the deoptimization of the environment localize everything

If you really want that functionality (is it really that important?) then you can get the script instance using the path returned by debug.info. Using setfenv turns off tons of Luau optimizations, and your logging library should absolutely not be doing that.

I’m not trying to prove anything. I just want to make sure people on the forum don’t unknowingly fall into that trap.

This will be my final reply regarding this

The click to select is a quality of life (just like luau optimizations)
My main reason for using the selection feature is it saves a lot of time especially in debugging (which I imagine a lot of people can agree).

This is true assuming none of the ancestors of the calling script have changed (name/parent) after the calling function’s definition. The issue with this though is that it is static and does not update. Additionally this undermines the core reason of this implementation; having the click select feature. If you’re proposing to manually search for the Instance using the path it still would still be unreliable and inefficient (same reason I said it was technically true).

A solution though (which I personally wouldn’t use) would be to require the user to pass ScriptName with either a string or the calling scripts Instance which would essentially defeat the entire purpose of this implementation which is automatic retrieval of the calling script when not provided it.
(also a “ton” is an overstatement but understandable)

yes

TLDR: debug.info is not applicable to my implementation but your concerns are valid

3 Likes

A reworked version of this:

local LogTypes = {
	Error = "🛑 [ERROR]",
	Warn = "⚠️ [WARNING]",
	Print = "📃 [LOG]"
}

local function logMessage(logType, message, scriptName, line)
	--// Validate the log type
	if not LogTypes[logType] then
		error(string.format("%s No Log Type Found For '%s' | Errored From Script: %s", LogTypes.Error, logType, script.Name))
	end
	
	--// Format the script name & line (default to "nil" if not provided)
	local formattedScriptName = scriptName or "nil"
	local formattedLine = line or "nil"
	
	--// Construct the full log message
	local fullMessage = string.format(
		"%s %s | Logged From: %s (Line %d)",
		LogTypes[logType],
		message,
		formattedScriptName,
		formattedLine
	)

	--// Dispatch the appropriate logging function
	if logType == "Print" then
		print(fullMessage)
	elseif logType == "Error" then
		error(fullMessage)
	elseif logType == "Warn" then
		warn(fullMessage)
	end
end

return logMessage

Example Use Case:

StarterPlayer/StarterCharacterScripts/LocalScript.luau

local logService = require(game.ReplicatedStorage.Modules.LogService)

--// Check if the module loaded successfully
if logService == nil then
    warn("Failed to load LogService module!")
else
    print("LogService module loaded successfully!")
end

--// Log a warning message
logService("Warn", "This is a warn message.", script.Name, debug.info(1, "l"))

Explanation of Each Argument:

  1. Warn (logType):
  • This specifies the type of log message being generated. In this case, it’s a warning (Warn), which corresponds to the "⚠️ [WARNING]" entry in the LogTypes table.
  1. "This is a warn message." (message):
  • This is the actual content of the log message. It describes what is being logged, such as an event, error, or informational message.
  1. script.Name (scriptName):
  • This provides the name of the script that is calling the logService function. The script.Name property retrieves the name of the current script from the Roblox Explorer.
  • For example, if the script is named MainScript, this will pass "MainScript" as the scriptName.
  1. debug.info(1, "l") (line):
  • This retrieves the current line number in the script where the logService function is called.
  • debug.info(1, "l") works as follows:
    - The 1 indicates the level of the call stack to inspect. A value of 1 refers to the current function (the one making the call to debug.info).
    - The "l" specifies that you want to retrieve the line number.
  • For example, if the logService call is on line 15 of your script, this will pass 15 as the line argument.