palabres

Overview

Palabres is an opinionated logger, thought out to be compatible out-of-the-box with BEAM and every JavaScript runtimes, whether it’s in browser or not, in Node or Deno, etc. In it’s simplest form, Palabres will simply creates a logger configured according to your needs, and will take care of the rest. Palabres considers logs as structured logs, to help you add context to your logs, and help you in future debugging sessions.

Structured logs

Logs can simply be strings output to your standard output, and have various roles. Most of the time, it is used to help debugging, and to see what’s happening in real time in production servers. However, you can also use logs to perform some analytics. Or maybe you want to track some user activities. That’s where structured logs shine: instead of outputting simple strings, it’s possible to output complex, rich data. To simplify that usage, Palabres helps by providing constructors functions. You can use palabres.string, palabres.int or palabres.float to add a field, with its name and a value. They will automatically be part of the resulting log. In Palabres, every log is not only a text message, but also context data that you’re free to add to help in further usages. Those additionnal data are free, so it’s up to you to determine how to use them efficiently!

Handling logs

Because logs are handled in multiple manners nowadays, Palabres provides two output formats: as string and as JSON. In the former format, every string will be formatted like query strings, but in a human readable way: no weird ampersand, no escaping. You can enjoy structured format while keeping it short, simple, and still easy to parse if needed. However, for processing logs, Palabres provides an easier JSON format. JSON formatting in logs allows to simply read and process them, and is widly implemented. DataDog, AWS CloudWatch, PaperTrail or NewRelic, all of them handle JSON logs without further configuration. Doing so also allows you to build on top of Palabres, and add specific structured fields required in your log processor.

Default values

Palabres imposes 4 standard data in every log: the level of the log, its timestamp, an ID in UUID format, and an arbitrary message string. You don’t have anything to do to add timestamp, id and level, they will automatically be added to every log you create. They’re all here to help you in future log processing sessions. In the worst case, they can be safely ignored. In the best case, they help you figure out what’s happening, when an where.

Arbitrary text messages are parts of log, and instead of putting them in a specific field, they’re simply dumped as is in the string format, or under the message field in JSON.

Usage Example

import palabres
import palabres/options

pub fn configure_logger() {
  use json_output <- result.try(is_json_output())
  options.defaults()
  |> options.json(json_output)
  |> palabres.configure
}

pub fn log_message(message, user) {
  palabres.info(message)
  |> palabres.string("node", get_node_info())
  |> palabres.string("user_id", user.id)
  |> palabres.int("user_age", user.age)
  |> palabres.log
}

fn is_json_output() {
  use output <- result.try(envoy.get("JSON_OUTPUT"))
  use <- bool.guard(when: json_output != "true", return: False)
  True
}

How does it work?

Behind the scenes, Palabres has two different behaviours, depending on your target. When targetting Erlang, Palabres will act as a logger layer, and will take care of the formatting, filtering, and helping to deal with logger for you. You don’t need to dive in its interface, Palabres got you covered. You can use Palabres to create your logs, but nothing stops you to use something like wisp.log_info in your code. In that case, Palabres can still intervene and apply styling. That allow you to write simple logs functions, while still leveraging structured logs. Because Palabres relies on logger, you can rather easily interact with Palabres in Erlang, provided you know how to use logger.

On JavaScript though, Palabres instanciate a full-fledged, custom logger. Because JavaScript does not have any logger concept in its root, Palabres provides a logger with similar abilities than logger on BEAM. As such, every logs should go through the Palabres package, otherwise the logger won’t be able to format them accordingly.

Types

Log instance, holding the structured data. Can be instanciated with corresponding “level” functions: emergency, alert, critical, error, warning, notice, info, debug.

pub fn log() {
  palabres.info("This is a log")
  |> palabres.log
}
pub opaque type Log

Functions

pub fn alert(message: String) -> Log

Creates an Alert Log instance. Should be used as a starting point to create a log.

pub fn log() {
  palabres.alert("Example message")
  |> palabres.log
}
pub fn at(
  log: Log,
  module module: String,
  function function: String,
) -> Log

Debug information, used to easily pinpoint a location in your code. at should point to a module and a function, and will display in your logs a data looking like at=module.function with proper colored output if activated.

pub fn log() {
  palabres.info("Example message")
  |> palabres.at("my_logger", "log")
  |> palabres.log
  // Turns in:
  //   level=info when=2024-12-15T15:59:17Z id=3a5c0fc2-5f74-4796-84df-cbfe4000eef6 at=my_logger.log Example message
}
pub fn configure(options: Options) -> Nil

Configure Palabres logger. Because logger is a singleton, it only needs to be configured once at startup. Select your options, and run configurations, to get Palabres logger running. Each logger call will then go through Palabres on BEAM. On JavaScript, calling palabres functions are still required.

pub fn critical(message: String) -> Log

Creates a Critical Log instance. Should be used as a starting point to create a log.

pub fn log() {
  palabres.critical("Example message")
  |> palabres.log
}
pub fn debug(message: String) -> Log

Creates a Debug Log instance. Should be used as a starting point to create a log.

pub fn log() {
  palabres.debug("Example message")
  |> palabres.log
}
pub fn emergency(message: String) -> Log

Creates an Emergency Log instance. Should be used as a starting point to create a log.

pub fn log() {
  palabres.emergency("Example message")
  |> palabres.log
}
pub fn error(message: String) -> Log

Creates an Error Log instance. Should be used as a starting point to create a log.

pub fn log() {
  palabres.error("Example message")
  |> palabres.log
}
pub fn float(log: Log, key: String, value: Float) -> Log

Add a float field to your structured data.

pub fn info(message: String) -> Log

Creates an Info Log instance. Should be used as a starting point to create a log.

pub fn log() {
  palabres.info("Example message")
  |> palabres.log
}
pub fn int(log: Log, key: String, value: Int) -> Log

Add an int field to your structured data.

pub fn log(log_: Log) -> Nil

Run your log and make it display in your log output.

pub fn log_alert(message: String) -> Nil

Log with message at level Alert directly.
Avoid to write the full log pipeline when you just need to write a single message.

palabres.log_alert("Example message")
pub fn log_critical(message: String) -> Nil

Log with message at level Critical directly.
Avoid to write the full log pipeline when you just need to write a single message.

palabres.log_critical("Example message")
pub fn log_debug(message: String) -> Nil

Log with message at level Debug directly.
Avoid to write the full log pipeline when you just need to write a single message.

palabres.log_debug("Example message")
pub fn log_emergency(message: String) -> Nil

Log with message at level Emergency directly.
Avoid to write the full log pipeline when you just need to write a single message.

palabres.log_emergency("Example message")
pub fn log_error(message: String) -> Nil

Log with message at level Error directly.
Avoid to write the full log pipeline when you just need to write a single message.

palabres.log_error("Example message")
pub fn log_info(message: String) -> Nil

Log with message at level Info directly.
Avoid to write the full log pipeline when you just need to write a single message.

palabres.log_info("Example message")
pub fn log_notice(message: String) -> Nil

Log with message at level Notice directly.
Avoid to write the full log pipeline when you just need to write a single message.

palabres.log_notice("Example message")
pub fn log_request(
  req: Request(Connection),
  handler: fn() -> Response(Body),
) -> Response(Body)

Provides a middleware to display every incoming request for a Wisp server.
Use it in your router to log request with status code, path and method.

pub fn router(req: wisp.Request, ctx: context) {
  use <- palabres.log_request(req)
  route_request(req)
}
pub fn log_warning(message: String) -> Nil

Log with message at level Warning directly.
Avoid to write the full log pipeline when you just need to write a single message.

palabres.log_warning("Example message")
pub fn notice(message: String) -> Log

Creates a Notice Log instance. Should be used as a starting point to create a log.

pub fn log() {
  palabres.notice("Example message")
  |> palabres.log
}
pub fn string(log: Log, key: String, value: String) -> Log

Add a string field to your structured data.

pub fn warning(message: String) -> Log

Creates a Warning Log instance. Should be used as a starting point to create a log.

pub fn log() {
  palabres.warning("Example message")
  |> palabres.log
}
Search Document