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()
The entity identifies the sink. Each declared level identifies one log event.
Check yourself
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.