122 lines
3.6 KiB
Go
122 lines
3.6 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.tornberg.me/go-otel/internal/websocket"
|
|
)
|
|
|
|
// TestClickHouseStoreIntegration is skipped unless OTEL_CLICKHOUSE_TEST_ADDR
|
|
// points at a disposable ClickHouse (native port 9000). It exercises the real
|
|
// SQL: schema creation, Append, rollup-triggered stats and Search filters.
|
|
//
|
|
// Placeholder round-trips through AsyncInsert use wait_for_async_insert so
|
|
// reads below see the writes without sleeps.
|
|
func TestClickHouseStoreIntegration(t *testing.T) {
|
|
addr := os.Getenv("OTEL_CLICKHOUSE_TEST_ADDR")
|
|
if addr == "" {
|
|
t.Skip("set OTEL_CLICKHOUSE_TEST_ADDR=host:9000 (with a disposable DB) to run")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
// Unique database per run so repeated runs don't collide.
|
|
db := "otel_test_" + time.Now().UTC().Format("150405")
|
|
cfg := ClickHouseConfig{Addr: addr, Database: db, Username: os.Getenv("OTEL_CLICKHOUSE_TEST_USER"), Password: os.Getenv("OTEL_CLICKHOUSE_TEST_PASSWORD")}
|
|
s, err := NewClickHouseStore(ctx, cfg)
|
|
if err != nil {
|
|
t.Skipf("ClickHouse not reachable at %s: %v", addr, err)
|
|
}
|
|
defer func() {
|
|
_ = s.conn.Exec(ctx, "DROP DATABASE IF EXISTS "+db)
|
|
_ = s.Close()
|
|
}()
|
|
|
|
now := time.Now().UTC()
|
|
mk := func(minsAgo int, typ, service string, isErr bool, release string) websocket.HistoryEntry {
|
|
raw, _ := json.Marshal(map[string]interface{}{"n": minsAgo})
|
|
return websocket.HistoryEntry{
|
|
Type: typ,
|
|
Timestamp: now.Add(-time.Duration(minsAgo) * time.Minute).UnixNano(),
|
|
Data: raw,
|
|
}
|
|
}
|
|
entries := []websocket.HistoryEntry{
|
|
mk(200, "logs", "api", true, "v1.3"),
|
|
mk(190, "logs", "api", false, "v1.3"),
|
|
mk(100, "logs", "api", true, "v1.4"),
|
|
mk(90, "logs", "api", false, "v1.4"),
|
|
mk(30, "logs", "api", false, "v1.4"),
|
|
}
|
|
for i, e := range entries {
|
|
if err := s.Append(ctx, e); err != nil {
|
|
t.Fatalf("Append[%d]: %v", i, err)
|
|
}
|
|
}
|
|
// Flush async insert buffer before reading.
|
|
if err := s.conn.AsyncInsert(ctx, "INSERT INTO FUNCTION null('x UInt8') VALUES (0)", true); err != nil {
|
|
t.Fatalf("flush: %v", err)
|
|
}
|
|
|
|
// Search: newest first, limit respected.
|
|
res, err := s.Search(ctx, SearchOptions{Type: "logs"}, 3)
|
|
if err != nil {
|
|
t.Fatalf("Search: %v", err)
|
|
}
|
|
if len(res) != 3 {
|
|
t.Fatalf("expected 3 search results, got %d", len(res))
|
|
}
|
|
if res[0].Timestamp < res[2].Timestamp {
|
|
t.Errorf("expected newest first")
|
|
}
|
|
|
|
// Search: substring filter.
|
|
res, err = s.Search(ctx, SearchOptions{Query: "\"n\":200"}, 10)
|
|
if err != nil {
|
|
t.Fatalf("Search query: %v", err)
|
|
}
|
|
if len(res) != 1 {
|
|
t.Errorf("expected substring filter to match 1 entry, got %d", len(res))
|
|
}
|
|
|
|
// ErrorStats via the rollup MV: two hours with data, 2 errors total.
|
|
points, err := s.ErrorStats(ctx, "api", 24, false)
|
|
if err != nil {
|
|
t.Fatalf("ErrorStats: %v", err)
|
|
}
|
|
errSum, totSum := uint64(0), uint64(0)
|
|
for _, p := range points {
|
|
errSum += p.Errors
|
|
totSum += p.Total
|
|
}
|
|
if errSum != 2 || totSum != 5 {
|
|
t.Errorf("expected rollup totals errors=2 total=5, got errors=%d total=%d", errSum, totSum)
|
|
}
|
|
|
|
// ReleaseSeries: v1.3 has 2 entries (1 error), v1.4 has 3 (1 error).
|
|
series, err := s.ReleaseSeries(ctx, "api", 24)
|
|
if err != nil {
|
|
t.Fatalf("ReleaseSeries: %v", err)
|
|
}
|
|
var v13, v14 uint64
|
|
for _, p := range series["v1.3"] {
|
|
v13 += p.Total
|
|
}
|
|
for _, p := range series["v1.4"] {
|
|
v14 += p.Total
|
|
}
|
|
if v13 != 2 || v14 != 3 {
|
|
t.Errorf("expected per-release totals v1.3=2 v1.4=3, got %d/%d", v13, v14)
|
|
}
|
|
|
|
// Services.
|
|
svcs, err := s.Services(ctx)
|
|
if err != nil || len(svcs) == 0 || svcs[0] != "api" {
|
|
t.Errorf("expected services [api], got %v (err=%v)", svcs, err)
|
|
}
|
|
}
|