github.com/nikandfor/tlog@v0.21.5-0.20231108111739-3ef89426a96d/ext/tlclick/click.go (about) 1 package tlclick 2 3 import ( 4 "context" 5 "io" 6 "time" 7 8 "github.com/ClickHouse/clickhouse-go/v2/lib/driver" 9 "github.com/google/uuid" 10 "github.com/nikandfor/errors" 11 "github.com/nikandfor/tlog" 12 "github.com/nikandfor/tlog/convert" 13 "github.com/nikandfor/tlog/tlwire" 14 ) 15 16 type ( 17 UUID = uuid.UUID 18 19 Click struct { 20 c driver.Conn 21 22 KeyTimestamp string 23 KeySpan string 24 25 JSON *convert.JSON 26 27 b driver.Batch 28 lastFlush time.Time 29 30 spans []UUID 31 related []UUID 32 33 dataJSON []byte 34 labelsJSON []byte 35 36 labelsBuf []byte 37 labelsArr [][]byte 38 } 39 ) 40 41 func New(c driver.Conn) *Click { 42 d := &Click{ 43 c: c, 44 45 KeyTimestamp: tlog.KeyTimestamp, 46 KeySpan: tlog.KeySpan, 47 } 48 49 d.JSON = convert.NewJSON(nil) 50 d.JSON.AppendNewLine = false 51 52 return d 53 } 54 55 func (d *Click) Query(ctx context.Context, w io.Writer, q string) error { 56 return nil 57 } 58 59 func (d *Click) CreateTables(ctx context.Context) error { 60 err := d.c.Exec(ctx, `CREATE TABLE IF NOT EXISTS logs ( 61 tlog String, 62 json String, 63 64 labels String COMMENT 'json formatted', 65 _labels Array(String) COMMENT 'tlog pairs', 66 _labels_hash UInt64 MATERIALIZED cityHash64(arrayStringConcat(_labels)), 67 68 ts DateTime64(9, 'UTC'), 69 70 spans Array(UUID), 71 related Array(UUID), 72 73 timestamp String ALIAS visitParamExtractString(json, '_t'), 74 span String ALIAS toUUIDOrZero(visitParamExtractString(json, '_s')), 75 parent String ALIAS toUUIDOrZero(visitParamExtractString(json, '_p')), 76 caller String ALIAS visitParamExtractString(json, '_c'), 77 message String ALIAS visitParamExtractString(json, '_m'), 78 msg String ALIAS message, 79 event String ALIAS visitParamExtractString(json, '_k'), 80 elapsed Int64 ALIAS visitParamExtractInt(json, '_e'), 81 log_level Int8 ALIAS visitParamExtractInt(json, '_l'), 82 error String ALIAS visitParamExtractString(json, 'err'), 83 84 kvs Array(Tuple(String, String)) ALIAS arrayMap(k -> (k, JSONExtractRaw(json, k)), arrayFilter(k -> k NOT IN ('_s', '_t', '_c', '_m'), JSONExtractKeys(json))), 85 86 minute DateTime ALIAS toStartOfMinute(ts), 87 hour DateTime ALIAS toStartOfHour(ts), 88 day Date MATERIALIZED toStartOfDay(ts), 89 week Date ALIAS toStartOfWeek(ts), 90 month Date ALIAS toStartOfMonth(ts), 91 year Date ALIAS toStartOfYear(ts), 92 ) 93 ENGINE MergeTree 94 ORDER BY ts 95 PARTITION BY (day, _labels_hash) 96 `) 97 if err != nil { 98 return errors.Wrap(err, "logs") 99 } 100 101 err = d.c.Exec(ctx, `CREATE OR REPLACE VIEW spans AS 102 SELECT 103 span, 104 min(ts) AS start, 105 max(ts) AS end, 106 anyIf(message, event = 's') AS name, 107 round(anyIf(elapsed, event = 'f') / 1e9, 1) AS elapsed_s, 108 anyIf(error, event = 'f') AS error 109 FROM logs GROUP BY span 110 `) 111 if err != nil { 112 return errors.Wrap(err, "spans") 113 } 114 115 return nil 116 } 117 118 func (d *Click) Write(p []byte) (n int, err error) { 119 for n < len(p) { 120 n, err = d.writeEvent(p, n) 121 if err != nil { 122 return n, err 123 } 124 } 125 126 return 127 } 128 129 func (d *Click) writeEvent(p []byte, st int) (next int, err error) { 130 if tlog.If("dump") { 131 defer func() { 132 tlog.V("dump").Printw("event", "err", err, "msg", tlog.RawMessage(p)) 133 }() 134 } 135 136 ts, next, err := d.parseEvent(p, st) 137 if err != nil { 138 return st, errors.Wrap(err, "parse message") 139 } 140 141 if d.b == nil { 142 ctx := context.Background() 143 144 d.b, err = d.c.PrepareBatch(ctx, `INSERT INTO logs ( 145 tlog, json, 146 _labels, labels, 147 ts, 148 spans, related 149 ) VALUES`) 150 if err != nil { 151 return st, errors.Wrap(err, "prepare batch") 152 } 153 154 d.lastFlush = time.Now() 155 } 156 157 err = d.b.Append( 158 p[st:next], d.dataJSON, 159 d.labelsArr, d.labelsJSON, 160 time.Unix(0, ts).UTC(), 161 d.spans, d.related, 162 ) 163 if err != nil { 164 return st, errors.Wrap(err, "append row") 165 } 166 167 if time.Since(d.lastFlush) > 1000*time.Millisecond { 168 err = d.Flush() 169 if err != nil { 170 return st, errors.Wrap(err, "flush") 171 } 172 } 173 174 return next, nil 175 } 176 177 func (d *Click) Flush() (err error) { 178 if d.b != nil { 179 e := d.b.Send() 180 if err == nil { 181 err = errors.Wrap(e, "flush batch") 182 } 183 184 d.b = nil 185 } 186 187 return err 188 } 189 190 func (d *Click) Close() error { 191 return d.Flush() 192 } 193 194 func (d *Click) parseEvent(p []byte, st int) (ts int64, i int, err error) { 195 var dec tlwire.Decoder 196 197 d.spans = d.spans[:0] 198 d.related = d.related[:0] 199 200 d.dataJSON = d.dataJSON[:0] 201 d.labelsJSON = d.labelsJSON[:0] 202 203 d.labelsBuf = d.labelsBuf[:0] 204 d.labelsArr = d.labelsArr[:0] 205 206 defer func() { 207 if len(d.dataJSON) != 0 { 208 d.dataJSON = append(d.dataJSON, '}') 209 } 210 211 if len(d.labelsJSON) != 0 { 212 d.labelsJSON = append(d.labelsJSON, '}') 213 } 214 }() 215 216 tag, els, i := dec.Tag(p, st) 217 if tag != tlwire.Map { 218 err = errors.New("expected map") 219 return 220 } 221 222 var k []byte 223 var sub int64 224 var end int 225 226 for el := 0; els == -1 || el < int(els); el++ { 227 if els == -1 && dec.Break(p, &i) { 228 break 229 } 230 231 st := i 232 233 k, i = dec.Bytes(p, i) 234 if len(k) == 0 { 235 err = errors.New("empty key") 236 return 237 } 238 239 tag, sub, end = dec.SkipTag(p, i) 240 241 { 242 var jb []byte 243 244 if tag == tlwire.Semantic && sub == tlog.WireLabel { 245 jb = d.labelsJSON 246 } else { 247 jb = d.dataJSON 248 } 249 250 if len(jb) == 0 { 251 jb = append(jb, '{') 252 } else { 253 jb = append(jb, ',') 254 } 255 256 jb, _ = d.JSON.ConvertValue(jb, p, st) 257 258 jb = append(jb, ':') 259 260 jb, _ = d.JSON.ConvertValue(jb, p, i) 261 262 if tag == tlwire.Semantic && sub == tlog.WireLabel { 263 d.labelsJSON = jb 264 } else { 265 d.dataJSON = jb 266 } 267 } 268 269 if tag != tlwire.Semantic { 270 i = end 271 continue 272 } 273 274 switch { 275 case sub == tlwire.Time && string(k) == d.KeyTimestamp: 276 ts, i = dec.Timestamp(p, i) 277 case sub == tlog.WireLabel: 278 // labels = crc32.Update(labels, crc32.IEEETable, p[st:end]) 279 280 lst := len(d.labelsBuf) 281 d.labelsBuf = append(d.labelsBuf, p[st:end]...) 282 d.labelsArr = append(d.labelsArr, d.labelsBuf[lst:]) 283 case sub == tlog.WireID: 284 var id tlog.ID 285 _ = id.TlogParse(p, i) 286 287 u := UUID(id) 288 289 //tlog.Printw("parsed id", "id", id, "key", string(k), "key_span", d.KeySpan) 290 291 if string(k) == d.KeySpan { 292 d.spans = append(d.spans, u) 293 } else { 294 d.related = append(d.related, u) 295 } 296 } 297 298 i = end 299 } 300 301 return 302 }