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

Evaluator.pudu

Pudu233 lines11.4 KB

GitHub ↗
1/** @Log.Expressions.Evaluator.Engine — expressions evaluated against an event */2module PuduLangLog.Expressions.Evaluator34import Std.Decimal as Decimal5import Std.List as List6import Std.Math.Float as Float7import Std.Option as Option8import PuduLangLog.Domain.Display as Display9import PuduLangLog.Domain.EventId as EventId10import PuduLangLog.Domain.Levels as Levels11import PuduLangLog.Domain.Properties as Properties12import PuduLangLog.Expressions.Functions as Functions13import PuduLangLog.Expressions.Syntax as Syntax14import PuduLangLog.Expressions.Values as Values15import PuduLangLog as Log1617/** @Log.Expressions.Evaluator.Scope — the event, bound names, and the present moment */18export type Scope = { event: Log.Event, locals: Array[(Str, Log.Value)], now: Log.Timestamp, referenced: Array[Str] }1920/// The within of an event with no bound names.21export fn scopeOf(event: &Log.Event, now: Log.Timestamp) -> Scope { Scope{event: *event, locals: [], now: now, referenced: []} }2223/// The value of an expression, or none when it is undefined.24export fn evaluate(expression: &Syntax.Expression, within: &Scope) -> Option[Log.Value] {25  match expression {26    case Syntax.Constant(held) => Some(held)27    case Syntax.Name(name) => nameOf(name, within)28    case Syntax.Member(target, name) => Functions.elementAt(&evaluate(&target, within) ?, &Log.Scalar(Log.Text(name)), false)29    case Syntax.Index(target, key) => Functions.elementAt(&evaluate(&target, within) ?, &evaluate(&key, within) ?, false)30    case Syntax.Wildcard(_, _) => None31    case Syntax.Call(name, arguments, ignoring) => {32      if let Some(expanded) = comprehension(expression, within) { return expanded }33      if name == "inspect" && arguments == [Syntax.Name("@x")] { return failureOf(&within.event.failure) }34      let values = arguments.map(|argument: Syntax.Expression| evaluate(&argument, within))35      if name == "rest" { return restOf(within, Values.isTrue(&Option.flatten(List.first(&values)))) }36      Functions.call(name, &values, ignoring, &within.now)37    }38    case Syntax.Unary(operator, operand) => {39      let held = evaluate(&operand, within)40      if operator == "not" {41        return match held {42          case Some(Log.Scalar(Log.Boolean(flag))) => Values.boolean(!flag)43          case _ => None44        }45      }46      Values.number(Decimal.zero() - Values.decimalOf(&held) ?)47    }48    case Syntax.Binary(operator, left, right, ignoring) => {49      if let Some(expanded) = comprehension(expression, within) { return expanded }50      binary(operator, &left, &right, ignoring, within)51    }52    case Syntax.ArrayOf(elements) => {53      var items: Array[Log.Value] = []54      for element in elements {55        match element {56          case Syntax.Item(item) => { if let Some(held) = evaluate(&item, within) { items = items.push(held) } }57          case Syntax.SpreadItems(spread) => { items = items.concat(Option.unwrapOr(Values.itemsOf(&evaluate(&spread, within)), [])) }58        }59      }60      Some(Log.Sequence(items))61    }62    case Syntax.ObjectOf(fields) => {63      var members: Array[Log.Property] = []64      for field in fields {65        match field {66          case Syntax.Pair(name, held) => {67            members = match evaluate(&held, within) {68              case Some(found) => Properties.put(&members, name, found)69              case None => Properties.remove(&members, name)70            }71          }72          case Syntax.SpreadMembers(spread) => {73            if let Some(Log.Structure(shape)) = evaluate(&spread, within) {74              for member in shape.properties { members = Properties.put(&members, member.name, member.value) }75            }76          }77        }78      }79      Some(Log.Structure(Log.Structure{tag: None, properties: members}))80    }81    case Syntax.Conditional(condition, then, otherwise) => if Values.isTrue(&evaluate(&condition, within)) { evaluate(&then, within) } else { evaluate(&otherwise, within) }82  }83}8485/// A name's value: a bound name first, then a built-in `@` property, then an event property.86fn nameOf(name: Str, within: &Scope) -> Option[Log.Value] {87  if let Some(bound) = List.find(&within.locals, |local: (Str, Log.Value)| local[0] == name) { return Some(bound[1]) }88  let event = &within.event89  match name {90    case "@t" => Some(Log.Scalar(Log.Moment(event.timestamp)))91    case "@m" => Values.text(Display.plain(&Display.message(&event.template, &event.properties, &Some("l"))))92    case "@mt" => Values.text(event.template.text)93    case "@l" => Values.text(Levels.name(event.level))94    case "@x" => Values.text(Display.failure(&event.failure ?))95    case "@p" => Some(Log.Structure(Log.Structure{tag: None, properties: event.properties}))96    case "@i" => Some(Log.Scalar(Log.Integer(Option.unwrapOr(convertInteger[Int](EventId.compute(event.template.text)), 0))))97    case "@r" => Some(Log.Sequence(event.template.holes.filter(|hole: Log.Hole| hole.format != None).map(|hole: Log.Hole| Log.Scalar(Log.Text(Display.plain(&Display.hole(&hole, &event.properties, false, false)))))))98    case "@tr" => Values.text(event.traceId ?)99    case "@sp" => Values.text(event.spanId ?)100    case _ => Properties.find(&event.properties, name)101  }102}103104/// The event's properties that the template does not name, and when `deep` also those the message105/// template names.106fn restOf(within: &Scope, deep: Bool) -> Option[Log.Value] {107  let named = within.event.template.holes.map(|hole: Log.Hole| hole.name)108  let kept = within.event.properties.filter(|property: Log.Property| !within.referenced.contains(property.name) && !(deep && named.contains(property.name)))109  Some(Log.Structure(Log.Structure{tag: None, properties: kept}))110}111112/// A failure as a structure of its kind, message, trace, and causes.113fn failureOf(problem: &Option[Log.Failure]) -> Option[Log.Value] {114  let found = (*problem) ?115  Some(Log.Structure(Log.Structure {116        tag: Some(found.kind),117        properties: [118          Log.Property{name: "Kind", value: Log.Scalar(Log.Text(found.kind))},119          Log.Property{name: "Message", value: Log.Scalar(Log.Text(found.message))},120          Log.Property{name: "Trace", value: Log.Sequence(found.trace.map(|line: Str| Log.Scalar(Log.Text(line))))},121          Log.Property{name: "Causes", value: Log.Sequence(found.causes.map(|cause: Log.Failure| Option.unwrapOr(failureOf(&Some(cause)), Log.Scalar(Log.Null))))}122        ]123      }))124}125126/// A binary operator applied to its operands.127fn binary(operator: Str, left: &Syntax.Expression, right: &Syntax.Expression, ignoring: Bool, within: &Scope) -> Option[Log.Value] {128  if operator == "and" { return Values.boolean(Values.isTrue(&evaluate(left, within)) && Values.isTrue(&evaluate(right, within))) }129  if operator == "or" { return Values.boolean(Values.isTrue(&evaluate(left, within)) || Values.isTrue(&evaluate(right, within))) }130  let first = evaluate(left, within)131  if operator == "is null" { return Values.boolean(first == None || first == Some(Log.Scalar(Log.Null))) }132  let second = evaluate(right, within)133  match operator {134    case "=" => Values.equals(&first, &second, ignoring)135    case "<>" => Option.map(Values.equals(&first, &second, ignoring), |equal: Log.Value| Log.Scalar(Log.Boolean(equal != Log.Scalar(Log.Boolean(true)))))136    case "like" => Values.boolean(Values.like(Values.textOf(&first) ?, Values.textOf(&second) ?, ignoring))137    case "in" => {138      let wanted = first ?139      Values.boolean(List.any(&Values.itemsOf(&second) ?, |item: Log.Value| Values.equal(&wanted, &item, ignoring)))140    }141    case "+" => Values.number(Values.decimalOf(&first) ? + Values.decimalOf(&second) ?)142    case "-" => Values.number(Values.decimalOf(&first) ? - Values.decimalOf(&second) ?)143    case "*" => Values.number(Values.decimalOf(&first) ? * Values.decimalOf(&second) ?)144    case "/" => Values.number(Decimal.divideHalfEven(Values.decimalOf(&first) ?, Values.decimalOf(&second) ?, 28) ?)145    case "%" => remainder(&first, &second)146    case "^" => {147      let base = Decimal.toFloat64(Values.decimalOf(&first) ?)148      let exponent = Decimal.toFloat64(Values.decimalOf(&second) ?)149      Some(Log.Scalar(Log.Real(Float.powf(base, exponent) ?)))150    }151    case _ => Values.compare(operator, &first, &second)152  }153}154155/// The remainder of two numbers, undefined for a zero divisor.156fn remainder(left: &Option[Log.Value], right: &Option[Log.Value]) -> Option[Log.Value] {157  let dividend = Values.decimalOf(left) ?158  let divisor = Values.decimalOf(right) ?159  let quotient = Decimal.truncate(Decimal.divideHalfEven(dividend, divisor, 28) ?)160  Values.number(dividend - quotient * divisor)161}162163/// A comparison or call over a wildcard, `Items[?].Price > 10` or `Tags[*] like 'a%'`, evaluated for164/// each element: true when any (`?`) or every (`*`) element makes it true. None when the node holds165/// no wildcard.166fn comprehension(expression: &Syntax.Expression, within: &Scope) -> Option[Option[Log.Value]] {167  let (target, quantifier) = wildcardIn(expression) ?168  let elements = match evaluate(&target, within) {169    case Some(Log.Sequence(items)) => items170    case Some(Log.Structure(shape)) => shape.properties.map(|member: Log.Property| member.value)171    case _ => { return Some(Values.boolean(false)) }172  }173  let results = elements.map(|element: Log.Value| Values.isTrue(&evaluate(&replaced(expression, &element), within)))174  Some(Values.boolean(if quantifier == Syntax.AnyOf { List.any(&results, |held: Bool| held) } else { List.all(&results, |held: Bool| held) }))175}176177/// The first wildcard reachable from a node's operands through member accesses and indexers.178fn wildcardIn(expression: &Syntax.Expression) -> Option[(Syntax.Expression, Syntax.Quantifier)] {179  let operands = match expression {180    case Syntax.Binary(_, left, right, _) => [left, right]181    case Syntax.Call(_, arguments, _) => arguments182    case _ => []183  }184  for operand in operands {185    if let Some(found) = chainWildcard(&operand) { return Some(found) }186  }187  None188}189190/// The wildcard at the root of a chain of member accesses and indexers.191fn chainWildcard(expression: &Syntax.Expression) -> Option[(Syntax.Expression, Syntax.Quantifier)] {192  match expression {193    case Syntax.Wildcard(target, quantifier) => Some((target, quantifier))194    case Syntax.Member(target, _) => chainWildcard(&target)195    case Syntax.Index(target, _) => chainWildcard(&target)196    case _ => None197  }198}199200/// The node with its first reachable wildcard replaced by an element.201fn replaced(expression: &Syntax.Expression, element: &Log.Value) -> Syntax.Expression {202  match expression {203    case Syntax.Binary(operator, left, right, ignoring) => {204      if chainWildcard(&left) != None { return Syntax.Binary(operator, substitute(&left, element), right, ignoring) }205      Syntax.Binary(operator, left, substitute(&right, element), ignoring)206    }207    case Syntax.Call(name, arguments, ignoring) => {208      var done = false209      var updated: Array[Syntax.Expression] = []210      for argument in arguments {211        if !done && chainWildcard(&argument) != None {212          updated = updated.push(substitute(&argument, element))213          done = true214        } else {215          updated = updated.push(argument)216        }217      }218      Syntax.Call(name, updated, ignoring)219    }220    case _ => *expression221  }222}223224/// A chain with its wildcard replaced by an element.225fn substitute(expression: &Syntax.Expression, element: &Log.Value) -> Syntax.Expression {226  match expression {227    case Syntax.Wildcard(_, _) => Syntax.Constant(*element)228    case Syntax.Member(target, name) => Syntax.Member(substitute(&target, element), name)229    case Syntax.Index(target, key) => Syntax.Index(substitute(&target, element), key)230    case _ => *expression231  }232}233