Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-log

Structured event logging for Pudu: message templates, enrichment, filtering, formatting, and sinks

0.1.0Apache-2.01

InstallClose

Display.pudu

Pudu161 lines6.9 KB

GitHub ↗
1/** @Log.Domain.Display.Module — values and messages rendered as styled text */2module PuduLangLog.Domain.Display34import Std.Decimal as Decimal5import PuduLangLog.Domain.Dates as Dates6import PuduLangLog.Domain.Json as Json7import PuduLangLog.Domain.Numbers as Numbers8import PuduLangLog.Domain.Padding as Padding9import PuduLangLog.Domain.Properties as Properties10import PuduLangLog as Log1112/** @Log.Domain.Display.Style — the role a piece of text plays */13export type Style = Plain | Secondary | Tertiary | Invalid | NullStyle | NameStyle | StringStyle | NumberStyle | BooleanStyle | ScalarStyle | LevelStyle(Log.Level)1415/** @Log.Domain.Display.Piece — text with its style */16export type Piece = { style: Style, text: Str }1718/// The type tag JSON renderings of messages and properties carry.19export const TYPE_TAG: Str = "$type"2021/// The pieces' text joined without styles.22export fn plain(pieces: &Array[Piece]) -> Str { pieces.map(|piece: Piece| piece.text).join("") }2324/// A scalar as display text under a format; text is quoted unless the format is `l`.25export fn scalar(atom: &Log.Scalar, format: &Option[Str]) -> Piece {26  match atom {27    case Log.Null => Piece{style: NullStyle, text: "null"}28    case Log.Boolean(flag) => Piece{style: BooleanStyle, text: if flag { "true" } else { "false" }}29    case Log.Integer(number) => Piece{style: NumberStyle, text: numeric(Numbers.formatInteger(number, formatText(format)), show(number), format)}30    case Log.Real(number) => Piece{style: NumberStyle, text: numeric(Numbers.formatReal(number, formatText(format)), Numbers.realText(number), format)}31    case Log.Exact(number) => Piece{style: NumberStyle, text: numeric(Numbers.formatExact(number, formatText(format)), Decimal.toText(number), format)}32    case Log.Text(text) => Piece{style: StringStyle, text: if *format == Some("l") { text } else { "\"" + text.replace("\"", "\\\"") + "\"" }}33    case Log.Moment(moment) => Piece{style: ScalarStyle, text: Dates.formatMoment(&moment, format)}34    case Log.Span(millis) => Piece{style: ScalarStyle, text: Dates.formatSpan(millis, format)}35    case Log.Binary(bytes) => Piece{style: ScalarStyle, text: Json.hexOf(&bytes)}36  }37}3839/// A formatted number, or its default text when there is no format or the format is not understood.40fn numeric(formatted: Option[Str], fallback: Str, format: &Option[Str]) -> Str {41  if *format == None { return fallback }42  match formatted {43    case Some(text) => text44    case None => fallback45  }46}4748/// The format's text, empty when there is none.49fn formatText(format: &Option[Str]) -> Str {50  match format {51    case Some(text) => text52    case None => ""53  }54}5556/// A value as display pieces. Sequence elements take the format; structure and dictionary members57/// render without one.58export fn value(held: &Log.Value, format: &Option[Str]) -> Array[Piece] {59  match held {60    case Log.Scalar(atom) => [scalar(&atom, format)]61    case Log.Sequence(items) => {62      var pieces = [punctuation("[")]63      var first = true64      for item in items {65        if !first { pieces = pieces.push(punctuation(", ")) }66        pieces = pieces.concat(value(&item, format))67        first = false68      }69      pieces.push(punctuation("]"))70    }71    case Log.Structure(shape) => structure(&shape)72    case Log.Dictionary(entries) => {73      var pieces = [punctuation("[")]74      var first = true75      for entry in entries {76        pieces = pieces.push(punctuation(if first { "(" } else { ", (" }))77        pieces = pieces.push(scalar(&entry.key, &None)).push(punctuation(": ")).concat(value(&entry.value, &None)).push(punctuation(")"))78        first = false79      }80      pieces.push(punctuation("]"))81    }82  }83}8485/// A structure as `Tag { Name: value, … }`.86fn structure(shape: &Log.Structure) -> Array[Piece] {87  var pieces: Array[Piece] = []88  if let Some(tag) = shape.tag { pieces = pieces.push(Piece{style: NameStyle, text: tag + " "}) }89  pieces = pieces.push(punctuation("\{ "))90  var first = true91  for property in shape.properties {92    if !first { pieces = pieces.push(punctuation(", ")) }93    pieces = pieces.push(Piece{style: NameStyle, text: property.name}).push(punctuation(": ")).concat(value(&property.value, &None))94    first = false95  }96  pieces.push(punctuation(if shape.properties.isEmpty() { "\}" } else { " \}" }))97}9899/// Punctuation between the parts of a value.100fn punctuation(text: Str) -> Piece { Piece{style: Tertiary, text: text} }101102/// A value as JSON, styled by the kind of value it is.103export fn json(held: &Log.Value) -> Piece {104  let style = match held {105    case Log.Scalar(atom) => scalar(&atom, &None).style106    case _ => Tertiary107  }108  Piece{style: style, text: Json.value(held, &Some(TYPE_TAG))}109}110111/// The template rendered with the properties. A format containing `l` writes text values without112/// quotes; one containing `j` writes values without their own format as JSON. A hole whose113/// property is missing keeps its raw text.114export fn message(template: &Log.Template, properties: &Array[Log.Property], format: &Option[Str]) -> Array[Piece] {115  let options = formatText(format)116  let literal = options.contains("l")117  let asJson = options.contains("j")118  var pieces: Array[Piece] = []119  for token in template.tokens {120    match token {121      case Log.Literal(text) => { pieces = pieces.push(Piece{style: Plain, text: text}) }122      case Log.Placeholder(placeholder) => { pieces = pieces.concat(hole(&placeholder, properties, literal, asJson)) }123    }124  }125  pieces126}127128/// One hole rendered from the properties and padded to its alignment.129export fn hole(placeholder: &Log.Hole, properties: &Array[Log.Property], literal: Bool, asJson: Bool) -> Array[Piece] {130  let found = match Properties.find(properties, placeholder.name) {131    case Some(held) => held132    case None => { return [Piece{style: Invalid, text: placeholder.raw}] }133  }134  let rendered = match found {135    case Log.Scalar(Log.Text(text)) => if literal { [Piece{style: StringStyle, text: text}] } else { value(&found, &placeholder.format) }136    case _ => if asJson && placeholder.format == None { [json(&found)] } else { value(&found, &placeholder.format) }137  }138  aligned(&rendered, &placeholder.alignment)139}140141/// The pieces with spaces added to reach an alignment's width.142export fn aligned(pieces: &Array[Piece], alignment: &Option[Log.Alignment]) -> Array[Piece] {143  let text = plain(pieces)144  let padding = Padding.padded(text, alignment).length() - text.length()145  if padding <= 0 { return *pieces }146  let spaces = Piece{style: Plain, text: " ".repeat(padding)}147  match alignment {148    case Some(shape) => if shape.left { pieces.push(spaces) } else { [spaces].concat(*pieces) }149    case None => *pieces150  }151}152153/// A failure as text: `Kind: message`, its trace lines indented under `at`, and each cause after154/// ` ---> ` in turn.155export fn failure(problem: &Log.Failure) -> Str {156  var lines = [if problem.message.isEmpty() { problem.kind } else { problem.kind + ": " + problem.message }]157  for frame in problem.trace { lines = lines.push("   at " + frame) }158  for cause in problem.causes { lines = lines.push(" ---> " + failure(&cause)) }159  lines.join("\n")160}161