/ chronicles / textblocks.nim
textblocks.nim
  1  ## Multi-line log format with indentation, similar to yaml
  2  import
  3    std/[strutils, terminal, typetraits],
  4    serialization/object_serialization,
  5    faststreams/[outputs, textio],
  6    ./[formats, log_output, options, textformats]
  7  
  8  export outputs, formats, textformats
  9  
 10  type LogRecord*[Output; format: static[FormatSpec]] = object of TextLogRecord[
 11    Output, format
 12  ]
 13    level: LogLevel
 14  
 15  const
 16    tupleOpenBracket = "(" & newLine
 17    tupleCloseBracket = ")" & newLine
 18    arrayOpenBracket = "[" & newLine
 19    arrayCloseBracket = "]" & newLine
 20    styleBright = terminal.styleBright # avoid terminal being unused in nocolors
 21  
 22  # Nim 2.0 compat
 23  template base(r: var LogRecord): untyped = TextLogRecord[r.Output, r.format](r)
 24  
 25  proc writeIndent(stream: OutputStream, depthLevel, extra: int) =
 26    for i in 0 ..< depthLevel:
 27      stream.write(indentStr)
 28    for i in 0 ..< extra:
 29      stream.write(" ")
 30  
 31  proc writeFieldName(r: var LogRecord, name: string) =
 32    when r.format.colors != NoColors:
 33      let (color, bright) = levelToStyle(r.level)
 34      base(r).setFgColor(color, bright)
 35    r.stream.write name
 36    base(r).resetColors()
 37    r.stream.write ": "
 38  
 39  template writeStyledValue(r: var LogRecord, body: untyped) =
 40    base(r).setFgColor(propColor, true)
 41    body
 42    base(r).resetColors()
 43  
 44  proc writeValueImpl[T](
 45      r: var LogRecord, value: T, depthLevel, extraIndent: int, deref: static bool
 46  ) =
 47    mixin chroniclesFormatItIMPL
 48  
 49    when value is ref Exception:
 50      r.writeValueImpl(value.msg, depthLevel, extraIndent, deref = false)
 51    elif value is ref:
 52      if value.isNil:
 53        r.writeStyledValue:
 54          r.stream.write("nil")
 55        r.stream.write(newLine)
 56      else:
 57        when deref:
 58          r.writeValueImpl(
 59            chroniclesFormatItIMPL value[], depthLevel, extraIndent, deref = false
 60          )
 61        else:
 62          # Avoid infinite recursion and other evils
 63          r.writeStyledValue:
 64            r.stream.write("...")
 65          r.stream.write(newLine)
 66    elif value is enum:
 67      r.writeStyledValue:
 68        r.stream.write($value)
 69      r.stream.write(newLine)
 70    elif value is array | seq | openArray:
 71      r.stream.write(arrayOpenBracket)
 72      for value in items(value):
 73        r.stream.writeIndent(depthLevel + 1, extraIndent)
 74        r.writeValueImpl(
 75          chroniclesFormatItIMPL(value), depthLevel + 1, extraIndent, deref = value is ref
 76        )
 77      r.stream.writeIndent(depthLevel, extraIndent)
 78      r.stream.write arrayCloseBracket
 79    elif value is object and isDefaultDollar($value):
 80      r.stream.write(newLine)
 81      enumInstanceSerializedFields(value, fieldName, fieldValue):
 82        r.stream.writeIndent(depthLevel + 1, extraIndent)
 83        r.writeFieldName(fieldName)
 84        r.writeValueImpl(
 85          chroniclesFormatItIMPL(fieldValue),
 86          depthLevel + 1,
 87          extraIndent + fieldName.len + 2,
 88          deref = false,
 89        )
 90    elif value is tuple and isDefaultDollar($value):
 91      r.stream.write(tupleOpenBracket)
 92      for name, v in fields(value):
 93        r.stream.writeIndent(depthLevel + 1)
 94        let extraIndent =
 95          extraIndent + (
 96            when isNameTuple(typeof(v)):
 97              r.writeFieldName(name)
 98              name.len + 2
 99            else:
100              discard name
101              0
102          )
103        r.writeValueImpl(
104          chroniclesFormatItIMPL(v), depthLevel + 1, extraIndent, deref = false
105        )
106      r.stream.writeIndent(depthLevel, extraIndent)
107      r.stream.write tupleCloseBracket
108    elif value is string | cstring:
109      r.writeStyledValue:
110        var first = true
111        for line in splitLines(value):
112          if not first:
113            r.stream.writeIndent(depthLevel, extraIndent)
114          r.stream.writeEscapedString(line)
115          r.stream.write(newLine)
116          first = false
117    elif compiles(r.stream.writeText(value)):
118      r.writeStyledValue:
119        r.stream.writeText(value)
120      r.stream.write(newLine)
121    elif compiles($value):
122      r.writeValueImpl($value, deref = false)
123    else:
124      const typeName = typetraits.name(T)
125      {.fatal: "The textblocks format does not support the '" & typeName & "' type".}
126  
127  template writeValue(r: var LogRecord, value: auto, depthLevel, extraIndent: int) =
128    mixin chroniclesFormatItIMPL
129    r.writeValueImpl(
130      chroniclesFormatItIMPL value, depthLevel, extraIndent, deref = value is ref
131    )
132  
133  proc setProperty*(r: var LogRecord, name: string, value: auto) =
134    r.stream.writeIndent(1, 0)
135    r.writeFieldName(name)
136    r.writeValue(value, 1, len(name) + 2)
137  
138  proc initLogRecord*(r: var LogRecord, level: LogLevel, topics, msg: string) =
139    r.stream = initOutputStream type(r)
140    r.level = level
141  
142    base(r).writeLogLevelMarker(level)
143    base(r).writeSpaceAndTs()
144  
145    r.stream.write(' ')
146    base(r).applyStyle(styleBright)
147    r.stream.write(msg)
148    base(r).resetColors()
149  
150    if topics.len > 0:
151      r.stream.write(' ')
152      base(r).setFgColor(propColor, false)
153      r.stream.write("topics")
154      base(r).resetColors()
155  
156      r.stream.write('=')
157      base(r).setFgColor(topicsColor, true)
158      r.stream.write('"')
159      r.stream.write(topics)
160      r.stream.write('"')
161  
162    r.stream.write(newLine)
163  
164  proc flushRecord*(r: var LogRecord) =
165    r.stream.write(newLine)
166  
167    r.output.append(r.stream)
168    r.output.flushOutput()