200 lines
6.1 KiB
Go
200 lines
6.1 KiB
Go
// normalized-preview reads a small sample from ClickHouse and prints the
|
|
// normalized rows without creating tables or writing any data.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
"git.tornberg.me/go-otel/internal/normalization"
|
|
"github.com/ClickHouse/clickhouse-go/v2"
|
|
)
|
|
|
|
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", 10, "raw envelopes to preview")
|
|
typeFilter := flag.String("type", "", "only preview one raw type: traces, logs, or metrics")
|
|
httpMode := flag.Bool("http", false, "use ClickHouse HTTP instead of native protocol")
|
|
summary := flag.Bool("summary", false, "aggregate by service, kind, and pattern instead of printing events")
|
|
flag.Parse()
|
|
if *httpMode {
|
|
previewHTTP(*addr, *database, *user, *password, *limit, *typeFilter, *summary)
|
|
return
|
|
}
|
|
|
|
conn, err := clickhouse.Open(&clickhouse.Options{Addr: []string{*addr}, Auth: clickhouse.Auth{Database: *database, Username: *user, Password: *password}})
|
|
if err != nil {
|
|
log.Fatalf("clickhouse open: %v", err)
|
|
}
|
|
defer conn.Close()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
if err := conn.Ping(ctx); err != nil {
|
|
log.Fatalf("clickhouse ping: %v", err)
|
|
}
|
|
if *typeFilter != "" && *typeFilter != "traces" && *typeFilter != "logs" && *typeFilter != "metrics" {
|
|
log.Fatalf("invalid -type %q", *typeFilter)
|
|
}
|
|
whereSQL := ""
|
|
if *typeFilter != "" {
|
|
whereSQL = fmt.Sprintf(" WHERE type = '%s'", *typeFilter)
|
|
}
|
|
rows, err := conn.Query(ctx, fmt.Sprintf("SELECT timestamp, type, data FROM %s.entries%s ORDER BY timestamp DESC LIMIT ?", *database, whereSQL), *limit)
|
|
if err != nil {
|
|
log.Fatalf("query entries: %v", err)
|
|
}
|
|
defer rows.Close()
|
|
encoder := json.NewEncoder(os.Stdout)
|
|
groups := map[string]*group{}
|
|
count := 0
|
|
for rows.Next() {
|
|
var timestamp time.Time
|
|
var entryType, data string
|
|
if err := rows.Scan(×tamp, &entryType, &data); err != nil {
|
|
log.Fatalf("scan entry: %v", err)
|
|
}
|
|
events, err := normalization.NormalizeEntry(entryType, timestamp.UnixNano(), []byte(data))
|
|
if err != nil {
|
|
log.Printf("skip %s at %s: %v", entryType, timestamp.Format(time.RFC3339), err)
|
|
continue
|
|
}
|
|
for _, event := range events {
|
|
if *summary {
|
|
addGroup(groups, event)
|
|
} else if err := encoder.Encode(event); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
count++
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if *summary {
|
|
printGroups(encoder, groups)
|
|
}
|
|
log.Printf("previewed %d normalized events from %d raw envelopes", count, *limit)
|
|
}
|
|
|
|
func previewHTTP(addr, database, user, password string, limit int, typeFilter string, summary bool) {
|
|
limitSQL := ""
|
|
orderSQL := " ORDER BY timestamp DESC"
|
|
whereSQL := ""
|
|
if typeFilter != "" {
|
|
if typeFilter != "traces" && typeFilter != "logs" && typeFilter != "metrics" {
|
|
log.Fatalf("invalid -type %q", typeFilter)
|
|
}
|
|
whereSQL = fmt.Sprintf(" WHERE type = '%s'", typeFilter)
|
|
}
|
|
if limit > 0 {
|
|
limitSQL = fmt.Sprintf(" LIMIT %d", limit)
|
|
} else {
|
|
// A full dry run must stream the table. Sorting every raw payload first
|
|
// can consume more memory than the ClickHouse query limit allows.
|
|
orderSQL = ""
|
|
}
|
|
query := fmt.Sprintf("SELECT toUnixTimestamp64Nano(timestamp) AS timestamp, type, data FROM %s.entries%s%s%s FORMAT JSONEachRow", database, whereSQL, orderSQL, limitSQL)
|
|
req, err := http.NewRequest(http.MethodGet, "http://"+addr+"/?query="+url.QueryEscape(query), nil)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
req.SetBasicAuth(user, password)
|
|
response, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
log.Fatalf("clickhouse HTTP request: %v", err)
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode/100 != 2 {
|
|
body, _ := io.ReadAll(response.Body)
|
|
log.Fatalf("clickhouse HTTP %s: %s", response.Status, body)
|
|
}
|
|
decoder := json.NewDecoder(response.Body)
|
|
encoder := json.NewEncoder(os.Stdout)
|
|
groups := map[string]*group{}
|
|
rawCount, eventCount := 0, 0
|
|
for {
|
|
var row struct {
|
|
Timestamp json.RawMessage `json:"timestamp"`
|
|
Type string `json:"type"`
|
|
Data string `json:"data"`
|
|
}
|
|
if err := decoder.Decode(&row); err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
rawCount++
|
|
timestamp := parseTimestamp(row.Timestamp)
|
|
events, err := normalization.NormalizeEntry(row.Type, timestamp, []byte(row.Data))
|
|
if err != nil {
|
|
log.Printf("skip %s: %v", row.Type, err)
|
|
continue
|
|
}
|
|
for _, event := range events {
|
|
if summary {
|
|
addGroup(groups, event)
|
|
} else if err := encoder.Encode(event); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
eventCount++
|
|
}
|
|
}
|
|
if summary {
|
|
printGroups(encoder, groups)
|
|
}
|
|
log.Printf("previewed %d normalized events from %d raw envelopes", eventCount, rawCount)
|
|
}
|
|
|
|
type group struct {
|
|
Service string `json:"service"`
|
|
Kind string `json:"kind"`
|
|
PatternSignature string `json:"patternSignature"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
func addGroup(groups map[string]*group, event normalization.Event) {
|
|
key := event.Service + "\x00" + event.Kind + "\x00" + event.PatternSignature
|
|
if groups[key] == nil {
|
|
groups[key] = &group{Service: event.Service, Kind: event.Kind, PatternSignature: event.PatternSignature}
|
|
}
|
|
groups[key].Count++
|
|
}
|
|
|
|
func printGroups(encoder *json.Encoder, groups map[string]*group) {
|
|
for _, item := range groups {
|
|
if err := encoder.Encode(item); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func parseTimestamp(raw json.RawMessage) int64 {
|
|
var text string
|
|
if json.Unmarshal(raw, &text) == nil {
|
|
value, _ := strconv.ParseInt(text, 10, 64)
|
|
return value
|
|
}
|
|
var value int64
|
|
_ = json.Unmarshal(raw, &value)
|
|
return value
|
|
}
|
|
|
|
func envOr(key, fallback string) string {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|