/ 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()