
Clef.pudu
Pudu147 lines6.1 KB
1/** @Log.Domain.Clef.Module — compact JSON lines read back into events */2module PuduLangLog.Domain.Clef34import Std.Decimal as Decimal5import Std.Json as Json6import Std.List as List7import Std.Option as Option8import Std.Time.Format as Format9import PuduLangLog.Domain.Display as Display10import PuduLangLog.Domain.Levels as Levels11import PuduLangLog.Domain.Parser as Parser12import PuduLangLog as Log131415const RESERVED: Array[Str] = ["@t", "@mt", "@m", "@l", "@x", "@i", "@r", "@tr", "@sp"]161718const TRACE_PREFIX: Str = " at "192021const CAUSE_PREFIX: Str = " ---> "222324export fn line(text: Str) -> Result[Log.Event, Str] {25 match Json.decode(text) {26 case Ok(document) => event(&document)27 case Err(problem) => Err("The line is not JSON: " + Json.explain(&problem))28 }29}3031323334export fn event(document: &Json.Json) -> Result[Log.Event, Str] {35 let members = match Json.asObject(document) {36 case Some(found) => found37 case None => { return Err("The event is not a JSON object") }38 }39 let stamp = textMember(&members, "@t") ?40 let timestamp = match stamp {41 case Some(written) => moment(written) ?42 case None => { return Err("The event has no `@t` timestamp") }43 }44 let level = match textMember(&members, "@l") ? {45 case Some(name) => Option.okOr(Levels.parse(name), "Unknown level `" + name + "`") ?46 case None => Log.Information47 }48 let rendered = Option.unwrapOr(textMember(&members, "@m") ?, "")49 let template = match textMember(&members, "@mt") ? {50 case Some(written) => written51 case None => rendered.replace("\{", "\{\{").replace("\}", "\}\}")52 }53 let failure = Option.map(textMember(&members, "@x") ?, |written: Str| failureOf(written))54 var properties: Array[Log.Property] = []55 for (name, held) in members {56 if name.startsWith("@@") {57 properties = properties.push(Log.Property{name: name.drop(1), value: valueOf(&held)})58 } else if !RESERVED.contains(name) {59 properties = properties.push(Log.Property{name: name, value: valueOf(&held)})60 }61 }62 Ok(Log.Event {63 timestamp: timestamp,64 level: level,65 template: Parser.parse(template),66 properties: properties,67 failure: failure,68 traceId: textMember(&members, "@tr") ?,69 spanId: textMember(&members, "@sp") ?70 })71}727374fn textMember(members: &Array[(Str, Json.Json)], name: Str) -> Result[Option[Str], Str] {75 match List.find(members, |member: (Str, Json.Json)| member[0] == name) {76 case Some(found) => match Json.asText(&found[1]) {77 case Some(text) => Ok(Some(text))78 case None => Err("The `" + name + "` member is not text")79 }80 case None => Ok(None)81 }82}838485fn moment(written: Str) -> Result[Log.Timestamp, Str] {86 let millis = match Format.fromRfc3339(written) {87 case Ok(found) => found88 case Err(_) => { return Err("The timestamp `" + written + "` is not RFC 3339") }89 }90 Ok(Log.Timestamp{millis: millis, offset: offsetOf(written)})91}929394fn offsetOf(written: Str) -> Int {95 let tail = written.drop(written.length() - 6)96 let hours = Option.unwrapOr(tail.slice(1, 3).toInt(), 0)97 let minutes = Option.unwrapOr(tail.slice(4, 6).toInt(), 0)98 if tail.startsWith("+") { hours * 60 + minutes } else if tail.startsWith("-") { 0 - hours * 60 - minutes } else { 0 }99}100101102103fn valueOf(held: &Json.Json) -> Log.Value {104 match held {105 case Json.Null => Log.Scalar(Log.Null)106 case Json.Boolean(flag) => Log.Scalar(Log.Boolean(flag))107 case Json.Number(number) => Log.Scalar(Log.Integer(number))108 case Json.Fractional(digits) => match Decimal.parse(digits) {109 case Some(exact) => Log.Scalar(Log.Exact(exact))110 case None => Log.Scalar(Log.Text(digits))111 }112 case Json.Text(text) => Log.Scalar(Log.Text(text))113 case Json.List(items) => Log.Sequence(items.map(|item: Json.Json| valueOf(&item)))114 case Json.Object(members) => {115 let tag = match List.find(&members, |member: (Str, Json.Json)| member[0] == Display.TYPE_TAG) {116 case Some(found) => Json.asText(&found[1])117 case None => None118 }119 let kept = members.filter(|member: (Str, Json.Json)| member[0] != Display.TYPE_TAG)120 Log.Structure(Log.Structure{tag: tag, properties: kept.map(|member: (Str, Json.Json)| Log.Property{name: member[0], value: valueOf(&member[1])})})121 }122 }123}124125126127fn failureOf(written: Str) -> Log.Failure {128 let lines = written.split("\n")129 let causeAt = match List.findIndex(&List.drop(&lines, 1), |text: Str| text.startsWith(CAUSE_PREFIX)) {130 case Some(found) => found + 1131 case None => lines.length()132 }133 let own = List.take(&lines, causeAt)134 let traceAt = Option.unwrapOr(List.findIndex(&own, |text: Str| text.startsWith(TRACE_PREFIX)), own.length())135 let heading = List.take(&own, traceAt).join("\n")136 let (kind, message) = match heading.indexOf(": ") {137 case -1 => (heading, "")138 case at => (heading.take(at), heading.drop(at + 2))139 }140 let trace = List.drop(&own, traceAt).map(|text: Str| text.drop(TRACE_PREFIX.length()))141 let causes = if causeAt == lines.length() { [] } else {142 let rest = List.drop(&lines, causeAt)143 [failureOf(rest.join("\n").drop(CAUSE_PREFIX.length()))]144 }145 Log.Failure{kind: kind, message: message, trace: trace, causes: causes}146}147