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