// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// Where in the source a record was made.
///
/// Every part may be unknown; a layout writes `?` for the ones that are, which
/// is what a call site without debug information looks like.
pub(all) struct Caller {
module_path : String?
func : String?
file : String?
line : Int32?
} derive(Eq, Debug)
///|
pub extend Caller with Debug::{to_repr}
///|
pub extend Caller with Eq::{equal, not_equal}
///|
/// A caller with no part known; set the parts that are.
pub fn Caller::new() -> Caller {
{ module_path: None, func: None, file: None, line: None, }
}
///|
/// The process and the deployment it is one instance of.
///
/// This is the only place where the library can learn who is writing: it is
/// passed in, never probed, so a test writes the identity it wants to check.
/// `offset` answers the seconds east of UTC at an instant, which is how a
/// local timestamp is written without the library owning a time zone database.
pub(all) struct Identity {
app : String
env : String
instance : String
host : String
pid : Int32
start : Int64
offset : (Int64) -> Int32
}
///|
/// An identity that names nobody: the deployment a library has not been told
/// about.
pub fn Identity::new() -> Identity {
{
app: "",
env: "",
instance: "",
host: "",
pid: 0,
start: 0L,
offset: fn(_) { 0 },
}
}
///|
/// An identity with the parts a deployment knows.
pub fn Identity::of(
app~ : String,
env~ : String = "",
instance~ : String = "",
host~ : String = "",
pid~ : Int32 = 0,
start~ : Int64 = 0L,
offset~ : (Int64) -> Int32 = fn(_) { 0 },
) -> Identity {
{ app, env, instance, host, pid, start, offset, }
}
///|
/// One log event, as every sink sees it.
///
/// There is no clock in here either: `time` is what was handed in, in
/// nanoseconds since the epoch, and 0 means nobody said.
pub(all) struct Record {
mut level : Level
target : String
mut msg : String
mut fields : Array[(String, Value)]
tags : Array[String]
mut caller : Caller
mut thread : String?
mut spans : String?
mut time : Int64
mut seq : Int64
}
///|
/// A record with everything but its level, target and message left empty.
pub fn Record::of(level : Level, target : String, msg : String) -> Record {
{
level,
target,
msg,
fields: [],
tags: [],
caller: Caller::new(),
thread: None,
spans: None,
time: 0L,
seq: 0L,
}
}
///|
/// A copy, with the arrays copied too, so an edit cannot be seen elsewhere.
pub fn Record::copy(self : Record) -> Record {
{
level: self.level,
target: self.target,
msg: self.msg,
fields: self.fields.copy(),
tags: self.tags.copy(),
caller: self.caller,
thread: self.thread,
spans: self.spans,
time: self.time,
seq: self.seq,
}
}
///|
/// The same record with `key` set: an earlier field of that name takes the new
/// value where it stands, and a new name is appended, so the order a reader
/// sees is scope, then logger, then the call.
pub fn Record::set(self : Record, key : String, value : Value) -> Record {
let out = self.copy()
for i, (k, _) in out.fields {
if k == key {
out.fields[i] = (key, value)
return out
}
}
out.fields.push((key, value))
out
}
///|
/// The same record with every pair set in turn.
pub fn Record::sets(self : Record, pairs : Array[(String, Value)]) -> Record {
let mut out = self
for pair in pairs {
out = out.set(pair.0, pair.1)
}
out
}
///|
/// The same record with `key` gone.
pub fn Record::unset(self : Record, key : String) -> Record {
let out = self.copy()
let kept : Array[(String, Value)] = []
for pair in out.fields {
if pair.0 != key {
kept.push(pair)
}
}
out.fields = kept
out
}
///|
/// The same record with another message.
pub fn Record::say(self : Record, msg : String) -> Record {
let out = self.copy()
out.msg = msg
out
}
///|
/// The same record at another level.
pub fn Record::at(self : Record, level : Level) -> Record {
let out = self.copy()
out.level = level
out
}
///|
/// The same record at an instant, in nanoseconds since the epoch.
pub fn Record::stamped(self : Record, time : Int64) -> Record {
let out = self.copy()
out.time = time
out
}
///|
/// The same record numbered `seq`.
pub fn Record::numbered(self : Record, seq : Int64) -> Record {
let out = self.copy()
out.seq = seq
out
}
///|
/// The same record carrying a source file and line.
pub fn Record::located(
self : Record,
file : String,
line~ : Int32 = 0,
) -> Record {
let out = self.copy()
out.caller = {
module_path: self.caller.module_path,
func: self.caller.func,
file: Some(file),
line: Some(line),
}
out
}
///|
/// The same record carrying a thread name.
pub fn Record::on(self : Record, thread : String) -> Record {
let out = self.copy()
out.thread = Some(thread)
out
}
///|
/// The last field called `key`.
pub fn Record::field(self : Record, key : String) -> Value? {
let mut found = None
for pair in self.fields {
if pair.0 == key {
found = Some(pair.1)
}
}
found
}
///|
/// Whether any field is called `key`.
pub fn Record::has(self : Record, key : String) -> Bool {
self.field(key) is Some(_)
}
///|
/// The same record carrying one more tag.
pub fn Record::tag(self : Record, tag : String) -> Record {
let out = self.copy()
out.tags.push(tag)
out
}
///|
/// Whether the record carries `tag`.
pub fn Record::tagged(self : Record, tag : String) -> Bool {
for t in self.tags {
if t == tag {
return true
}
}
false
}