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  }