Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Log

Sometimes you just want to simply log something human-readable. The log module provides a simple way to declare an entity that behaves as a general purpose log sink. At run-time, an instance of this entity will typically be leveraged as a general-purpose logging API. It could also receive messages from an already existing logging API that supports different backends.

The benefit is that those logs can follow the same export and analysis path as all other types of events, such that they can be correlated at analysis-time. For example, a UI could display them right alongside resource utilization metrics.

YAML model

Add an entry under the top-level logs: key to declare a log sink. Each item under levels creates a repeatable event with the same name. Quent adds a message: string attribute to every level event. There is no separate level attribute: an info message is an info event, and an error message is an error event.

The order of levels sets their ranks. The first level has rank 0, the next has rank 1, and so on.

quent: alpha
model: logging_sink

logs:
  AppLog:
    doc: Application logging sink.
    attributes:
      target: string
      file: string
      line: u32
      module: string
      thread_name: string
    levels:
      - name: trace
      - name: debug
      - name: info
        doc: Informational messages.
      - name: warning
        attributes:
          category: string
      - name: error
        attributes:
          error_code: u32

AppLog defines the levels and attributes supported by this kind of log sink. Each AppLog instance has its own runtime identity.

Quent adds a required message: string attribute to every level event, so each generated level method takes the message as an argument.

The target, file, line, module, and thread_name fields in this example are arbitrary attributes. Because they are declared directly under logs.AppLog.attributes, they are added to every level.

Attributes under a level are also arbitrary, but are added only to that level. Here, category is added to warning, while error_code is added to error.

Instrumentation API

The generated API has one method for each level. This example calls info and warning directly. A backend for an existing logging API could instead translate each log record into the matching generated method call.

use instrumentation::{AppLog, Context, LoggingSink, Noop};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let context = Context::<LoggingSink>::try_new(Noop)?;
    let log = context.observer::<AppLog>().handle();

    // A logging API would typically use a macro to capture `file!()`, `line!()`,
    // and any other call-site context required by the schema.
    log.info(
        "application started".to_owned(),
        "startup".to_owned(),
        file!().to_owned(),
        line!(),
        module_path!().to_owned(),
        "main".to_owned(),
    )?;
    log.warning(
        "retrying request".to_owned(),
        "network".to_owned(),
        file!().to_owned(),
        line!(),
        module_path!().to_owned(),
        "main".to_owned(),
        "transient".to_owned(),
    )?;

    Ok(())
}
#include "quent-tutorial-log-sink-cpp-bridge/gen/quent.hpp"

#include <cstdint>
#include <source_location>
#include <string>
#include <string_view>

using AppLog = quent::Handle<quent::AppLog>;

void info(const AppLog& log, std::string_view message, std::string_view target,
          std::source_location source = std::source_location::current()) {
  log.info(quent::app_log::Info{
      .message = std::string{message},
      .target = std::string{target},
      .file = source.file_name(),
      .line = static_cast<std::uint32_t>(source.line()),
      .module = source.function_name(),
      .thread_name = "main",
  });
}

void warning(const AppLog& log, std::string_view message,
             std::string_view target, std::string_view category,
             std::source_location source = std::source_location::current()) {
  log.warning(quent::app_log::Warning{
      .message = std::string{message},
      .target = std::string{target},
      .file = source.file_name(),
      .line = static_cast<std::uint32_t>(source.line()),
      .module = source.function_name(),
      .thread_name = "main",
      .category = std::string{category},
  });
}

int main() {
  auto context = quent::Context::none();
  auto log = context.app_log_observer()->handle();

  // Default arguments based on std::source_location capture each call site.
  info(log, "application started", "startup");
  warning(log, "retrying request", "network", "transient");

  return 0;
}
import quent_tutorial_log_sink as quent


def main() -> None:
    with quent.Context() as context:
        log = context.app_log_observer().handle()

        # A logging API would typically capture the file, line, and any other
        # call-site context required by the schema.
        log.info(
            message="application started",
            target="startup",
            file=__file__,
            line=13,
            module=__name__,
            thread_name="main",
        )
        log.warning(
            message="retrying request",
            target="network",
            file=__file__,
            line=21,
            module=__name__,
            thread_name="main",
            category="transient",
        )


if __name__ == "__main__":
    main()
Key point

The entity identifies the sink. Each declared level identifies one log event.

Check yourself

How does a log level appear in the generated schema?

Where do you declare an arbitrary attribute used by every level?

Advanced example

A log sink can have events unrelated to the events produced from the level definitions. This can be useful to add other, arbitrary events (e.g. initialized) to the sink.

Such events may even be required if you’re also leveraging the Reference Tree module, because that module adds the rule that all entities (indirectly) refer to some root entity in the shape of a tree.

Below is a typical example where a log sink is nested under an entity representing a process through the Operating System module:

entities:
  CoolApp:
    events:
      started:
        attributes:
          process: { os: process }

logs:
  AppLog:
    events:
      initialized:
        attributes:
          process: { scope-ref: CoolApp }
      flushed: {}
    levels:
      - name: info
      - name: error
      # etc.

Full code