174 lines
4.9 KiB
Go
174 lines
4.9 KiB
Go
// normalized-backfill previews or persists deterministic normalized events.
|
|
// It is read-only unless -write is explicitly supplied.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"sort"
|
|
"time"
|
|
|
|
"git.tornberg.me/go-otel/internal/normalization"
|
|
"git.tornberg.me/go-otel/internal/storage"
|
|
"github.com/ClickHouse/clickhouse-go/v2"
|
|
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
|
)
|
|
|
|
type patternCount struct {
|
|
Service string
|
|
Kind string
|
|
Pattern string
|
|
Count uint64
|
|
}
|
|
|
|
func main() {
|
|
addr := flag.String("addr", envOr("OTEL_CLICKHOUSE_ADDR", "127.0.0.1:9000"), "ClickHouse host:port")
|
|
database := flag.String("database", envOr("OTEL_CLICKHOUSE_DB", "otel"), "ClickHouse database")
|
|
user := flag.String("user", envOr("OTEL_CLICKHOUSE_USER", "default"), "ClickHouse user")
|
|
password := flag.String("password", os.Getenv("OTEL_CLICKHOUSE_PASSWORD"), "ClickHouse password")
|
|
limit := flag.Int("limit", 0, "raw envelopes to process; 0 means all")
|
|
since := flag.String("since", "", "inclusive UTC window start, RFC3339")
|
|
until := flag.String("until", "", "exclusive UTC window end, RFC3339")
|
|
batchSize := flag.Int("batch-size", 1000, "normalized events per insert batch")
|
|
write := flag.Bool("write", false, "create the table and insert normalized events")
|
|
flag.Parse()
|
|
if *batchSize < 1 {
|
|
log.Fatal("-batch-size must be positive")
|
|
}
|
|
start, end := parseWindow(*since, *until)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
|
defer cancel()
|
|
conn, err := clickhouse.Open(&clickhouse.Options{Addr: []string{*addr}, Auth: clickhouse.Auth{Database: *database, Username: *user, Password: *password}})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer conn.Close()
|
|
if err := conn.Ping(ctx); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if *write {
|
|
if err := storage.EnsureNormalizedSchema(ctx, conn, *database); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
whereSQL := ""
|
|
if !start.IsZero() {
|
|
whereSQL = fmt.Sprintf(" timestamp >= toDateTime64('%s', 9, 'UTC')", start.Format("2006-01-02 15:04:05.000000000"))
|
|
}
|
|
if !end.IsZero() {
|
|
if whereSQL != "" {
|
|
whereSQL += " AND"
|
|
}
|
|
whereSQL += fmt.Sprintf(" timestamp < toDateTime64('%s', 9, 'UTC')", end.Format("2006-01-02 15:04:05.000000000"))
|
|
}
|
|
if whereSQL != "" {
|
|
whereSQL = " WHERE" + whereSQL
|
|
}
|
|
limitSQL := ""
|
|
if *limit > 0 {
|
|
limitSQL = fmt.Sprintf(" LIMIT %d", *limit)
|
|
}
|
|
rows, err := conn.Query(ctx, fmt.Sprintf("SELECT timestamp, type, data FROM %s.entries%s%s", *database, whereSQL, limitSQL))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
counts := map[string]*patternCount{}
|
|
rawCount, eventCount := 0, 0
|
|
var batch driver.Batch
|
|
if *write {
|
|
batch, err = conn.PrepareBatch(ctx, storage.NormalizedInsertSQL(*database))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
flush := func() {
|
|
if batch != nil {
|
|
if err := batch.Send(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
batch = nil
|
|
}
|
|
}
|
|
for rows.Next() {
|
|
var timestamp time.Time
|
|
var entryType, data string
|
|
if err := rows.Scan(×tamp, &entryType, &data); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
rawCount++
|
|
events, err := normalization.NormalizeEntry(entryType, timestamp.UnixNano(), []byte(data))
|
|
if err != nil {
|
|
log.Printf("skip %s: %v", entryType, err)
|
|
continue
|
|
}
|
|
for _, event := range events {
|
|
eventCount++
|
|
key := event.Service + "\x00" + event.Kind + "\x00" + event.PatternSignature
|
|
if counts[key] == nil {
|
|
counts[key] = &patternCount{Service: event.Service, Kind: event.Kind, Pattern: event.PatternSignature}
|
|
}
|
|
counts[key].Count++
|
|
if *write {
|
|
if err := storage.AppendNormalizedEvent(batch, event); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if eventCount%*batchSize == 0 {
|
|
flush()
|
|
batch, err = conn.PrepareBatch(ctx, storage.NormalizedInsertSQL(*database))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
flush()
|
|
top := make([]*patternCount, 0, len(counts))
|
|
for _, item := range counts {
|
|
top = append(top, item)
|
|
}
|
|
sort.Slice(top, func(i, j int) bool { return top[i].Count > top[j].Count })
|
|
if len(top) > 20 {
|
|
top = top[:20]
|
|
}
|
|
fmt.Printf("raw_envelopes=%d normalized_events=%d unique_patterns=%d write=%t\n", rawCount, eventCount, len(top), *write)
|
|
for _, item := range top {
|
|
fmt.Printf("pattern_count=%d service=%s kind=%s pattern=%s\n", item.Count, item.Service, item.Kind, item.Pattern)
|
|
}
|
|
}
|
|
|
|
func envOr(key, fallback string) string {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func parseWindow(since, until string) (time.Time, time.Time) {
|
|
parse := func(value, name string) time.Time {
|
|
if value == "" {
|
|
return time.Time{}
|
|
}
|
|
parsed, err := time.Parse(time.RFC3339Nano, value)
|
|
if err != nil {
|
|
log.Fatalf("invalid -%s %q: %v", name, value, err)
|
|
}
|
|
return parsed.UTC()
|
|
}
|
|
start, end := parse(since, "since"), parse(until, "until")
|
|
if !start.IsZero() && !end.IsZero() && !start.Before(end) {
|
|
log.Fatal("-since must be before -until")
|
|
}
|
|
return start, end
|
|
}
|