diff --git a/.changeset/hungry-carpets-rule.md b/.changeset/hungry-carpets-rule.md new file mode 100644 index 00000000..a751ccb7 --- /dev/null +++ b/.changeset/hungry-carpets-rule.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/otel-collector": patch +--- + +feat: introduce HYPERDX_OTEL_EXPORTER_TABLES_TTL to support custom TTL configuration diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b887d48c..64a642ce 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -70,6 +70,28 @@ jobs: run: make ci-build - name: Run integration tests run: make ci-int + otel-unit-test: + timeout-minutes: 8 + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v46 + with: + files: | + packages/otel-collector/** + - name: Setup Go + if: steps.changed-files.outputs.any_changed == 'true' + uses: actions/setup-go@v5 + with: + go-version-file: packages/otel-collector/go.mod + cache-dependency-path: packages/otel-collector/go.sum + - name: Run unit tests + if: steps.changed-files.outputs.any_changed == 'true' + working-directory: ./packages/otel-collector + run: go test ./... otel-smoke-test: timeout-minutes: 8 runs-on: ubuntu-24.04 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 55b54fd6..ccb6a1a1 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -35,6 +35,7 @@ services: CUSTOM_OTELCOL_CONFIG_FILE: '/etc/otelcol-contrib/custom.config.yaml' # Uncomment to enable stdout logging for the OTel collector OTEL_SUPERVISOR_LOGS: 'true' + HYPERDX_OTEL_EXPORTER_TABLES_TTL: '24h' volumes: - ./docker/otel-collector/config.yaml:/etc/otelcol-contrib/config.yaml - ./docker/otel-collector/supervisor_docker.yaml.tmpl:/etc/otel/supervisor.yaml.tmpl diff --git a/docker/otel-collector/schema/seed/00002_otel_logs.sql b/docker/otel-collector/schema/seed/00002_otel_logs.sql index 97c68f5f..5cc414ed 100644 --- a/docker/otel-collector/schema/seed/00002_otel_logs.sql +++ b/docker/otel-collector/schema/seed/00002_otel_logs.sql @@ -38,6 +38,6 @@ ENGINE = MergeTree PARTITION BY toDate(TimestampTime) PRIMARY KEY (ServiceName, TimestampTime) ORDER BY (ServiceName, TimestampTime, Timestamp) -TTL TimestampTime + toIntervalDay(30) +TTL TimestampTime + ${TABLES_TTL} SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1; diff --git a/docker/otel-collector/schema/seed/00003_otel_metrics.sql b/docker/otel-collector/schema/seed/00003_otel_metrics.sql index 9570291e..b5e0b1d2 100644 --- a/docker/otel-collector/schema/seed/00003_otel_metrics.sql +++ b/docker/otel-collector/schema/seed/00003_otel_metrics.sql @@ -31,7 +31,9 @@ CREATE TABLE IF NOT EXISTS ${DATABASE}.otel_metrics_gauge ) ENGINE = MergeTree PARTITION BY toDate(TimeUnix) -ORDER BY (ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix)); +ORDER BY (ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix)) +TTL toDateTime(TimeUnix) + ${TABLES_TTL} +SETTINGS ttl_only_drop_parts = 1; -- +goose StatementBegin CREATE TABLE IF NOT EXISTS ${DATABASE}.otel_metrics_sum @@ -68,7 +70,9 @@ CREATE TABLE IF NOT EXISTS ${DATABASE}.otel_metrics_sum ) ENGINE = MergeTree PARTITION BY toDate(TimeUnix) -ORDER BY (ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix)); +ORDER BY (ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix)) +TTL toDateTime(TimeUnix) + ${TABLES_TTL} +SETTINGS ttl_only_drop_parts = 1; -- +goose StatementEnd -- +goose StatementBegin @@ -110,7 +114,9 @@ CREATE TABLE IF NOT EXISTS ${DATABASE}.otel_metrics_histogram ) ENGINE = MergeTree PARTITION BY toDate(TimeUnix) -ORDER BY (ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix)); +ORDER BY (ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix)) +TTL toDateTime(TimeUnix) + ${TABLES_TTL} +SETTINGS ttl_only_drop_parts = 1; -- +goose StatementEnd -- +goose StatementBegin @@ -156,7 +162,9 @@ CREATE TABLE IF NOT EXISTS ${DATABASE}.otel_metrics_exponential_histogram ) ENGINE = MergeTree PARTITION BY toDate(TimeUnix) -ORDER BY (ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix)); +ORDER BY (ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix)) +TTL toDateTime(TimeUnix) + ${TABLES_TTL} +SETTINGS ttl_only_drop_parts = 1; -- +goose StatementEnd -- +goose StatementBegin @@ -190,6 +198,8 @@ CREATE TABLE IF NOT EXISTS ${DATABASE}.otel_metrics_summary ) ENGINE = MergeTree PARTITION BY toDate(TimeUnix) -ORDER BY (ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix)); +ORDER BY (ServiceName, MetricName, Attributes, toUnixTimestamp64Nano(TimeUnix)) +TTL toDateTime(TimeUnix) + ${TABLES_TTL} +SETTINGS ttl_only_drop_parts = 1; -- +goose StatementEnd diff --git a/docker/otel-collector/schema/seed/00004_hyperdx_sessions.sql b/docker/otel-collector/schema/seed/00004_hyperdx_sessions.sql index 5bcb8d69..89077b83 100644 --- a/docker/otel-collector/schema/seed/00004_hyperdx_sessions.sql +++ b/docker/otel-collector/schema/seed/00004_hyperdx_sessions.sql @@ -30,6 +30,6 @@ ENGINE = MergeTree PARTITION BY toDate(TimestampTime) PRIMARY KEY (ServiceName, TimestampTime) ORDER BY (ServiceName, TimestampTime, Timestamp) -TTL TimestampTime + toIntervalDay(30) +TTL TimestampTime + ${TABLES_TTL} SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1; diff --git a/docker/otel-collector/schema/seed/00005_otel_traces.sql b/docker/otel-collector/schema/seed/00005_otel_traces.sql index bae16684..98341a67 100644 --- a/docker/otel-collector/schema/seed/00005_otel_traces.sql +++ b/docker/otel-collector/schema/seed/00005_otel_traces.sql @@ -36,6 +36,6 @@ CREATE TABLE IF NOT EXISTS ${DATABASE}.otel_traces ENGINE = MergeTree PARTITION BY toDate(Timestamp) ORDER BY (ServiceName, SpanName, toDateTime(Timestamp)) -TTL toDate(Timestamp) + toIntervalDay(30) +TTL toDate(Timestamp) + ${TABLES_TTL} SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1; diff --git a/packages/api/src/opamp/controllers/opampController.ts b/packages/api/src/opamp/controllers/opampController.ts index 21893706..b5d5dea3 100644 --- a/packages/api/src/opamp/controllers/opampController.ts +++ b/packages/api/src/opamp/controllers/opampController.ts @@ -198,7 +198,7 @@ export const buildOtelCollectorConfig = (teams: ITeam[]): CollectorConfig => { database: '${env:HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE}', username: '${env:CLICKHOUSE_USER}', password: '${env:CLICKHOUSE_PASSWORD}', - ttl: '720h', + ttl: '${env:HYPERDX_OTEL_EXPORTER_TABLES_TTL:-720h}', logs_table_name: 'hyperdx_sessions', timeout: '5s', create_schema: @@ -215,7 +215,7 @@ export const buildOtelCollectorConfig = (teams: ITeam[]): CollectorConfig => { database: '${env:HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE}', username: '${env:CLICKHOUSE_USER}', password: '${env:CLICKHOUSE_PASSWORD}', - ttl: '720h', + ttl: '${env:HYPERDX_OTEL_EXPORTER_TABLES_TTL:-720h}', timeout: '5s', create_schema: '${env:HYPERDX_OTEL_EXPORTER_CREATE_LEGACY_SCHEMA:-false}', diff --git a/packages/otel-collector/cmd/migrate/main.go b/packages/otel-collector/cmd/migrate/main.go index 0cb5e8c9..61fbc212 100644 --- a/packages/otel-collector/cmd/migrate/main.go +++ b/packages/otel-collector/cmd/migrate/main.go @@ -15,6 +15,7 @@ import ( "os" "path/filepath" "sort" + "strconv" "strings" "time" @@ -31,6 +32,9 @@ type Config struct { Password string Database string + // Table TTL (Go duration string, e.g. "720h") + TablesTTL string + // TLS settings TLSCAFile string TLSCertFile string @@ -50,6 +54,7 @@ func loadConfig() (*Config, error) { User: getEnv("CLICKHOUSE_USER", "default"), Password: getEnv("CLICKHOUSE_PASSWORD", ""), Database: getEnv("HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE", "default"), + TablesTTL: getEnv("HYPERDX_OTEL_EXPORTER_TABLES_TTL", "720h"), TLSCAFile: getEnv("CLICKHOUSE_TLS_CA_FILE", ""), TLSCertFile: getEnv("CLICKHOUSE_TLS_CERT_FILE", ""), TLSKeyFile: getEnv("CLICKHOUSE_TLS_KEY_FILE", ""), @@ -215,9 +220,46 @@ func createClickHouseDB(cfg *Config) (*sql.DB, error) { return db, nil } +// parseTTLDuration parses a duration string that supports days ("30d") in +// addition to the standard Go duration format ("720h", "90m", "3600s"). +func parseTTLDuration(s string) (time.Duration, error) { + // Handle "d" suffix (days) which Go's time.ParseDuration doesn't support + if strings.HasSuffix(s, "d") { + days, err := strconv.Atoi(strings.TrimSuffix(s, "d")) + if err != nil { + return 0, fmt.Errorf("invalid duration %q: %w", s, err) + } + return time.Duration(days) * 24 * time.Hour, nil + } + return time.ParseDuration(s) +} + +// ttlToClickHouseInterval converts a duration string (e.g. "30d", "720h", +// "90m") to a ClickHouse interval expression, following the same approach as +// the upstream otel-collector-contrib ClickHouse exporter's GenerateTTLExpr. +func ttlToClickHouseInterval(ttl string) (string, error) { + d, err := parseTTLDuration(ttl) + if err != nil { + return "", fmt.Errorf("invalid TTL duration %q: %w", ttl, err) + } + if d <= 0 { + return "", fmt.Errorf("TTL must be positive, got %q", ttl) + } + switch { + case d%(24*time.Hour) == 0: + return fmt.Sprintf("toIntervalDay(%d)", d/(24*time.Hour)), nil + case d%time.Hour == 0: + return fmt.Sprintf("toIntervalHour(%d)", d/time.Hour), nil + case d%time.Minute == 0: + return fmt.Sprintf("toIntervalMinute(%d)", d/time.Minute), nil + default: + return fmt.Sprintf("toIntervalSecond(%d)", d/time.Second), nil + } +} + // processSchemaDir creates a temporary directory with SQL files that have -// the ${DATABASE} macro replaced with the actual database name -func processSchemaDir(schemaDir, database string) (string, error) { +// the ${DATABASE} and ${TABLES_TTL} macros replaced with actual values +func processSchemaDir(schemaDir, database, tablesTTLExpr string) (string, error) { tempDir, err := os.MkdirTemp("", "schema-*") if err != nil { return "", fmt.Errorf("failed to create temp directory: %w", err) @@ -247,8 +289,9 @@ func processSchemaDir(schemaDir, database string) (string, error) { return fmt.Errorf("failed to read file %s: %w", path, err) } - // Replace ${DATABASE} macro with actual database name + // Replace macros with actual values processedContent := strings.ReplaceAll(string(content), "${DATABASE}", database) + processedContent = strings.ReplaceAll(processedContent, "${TABLES_TTL}", tablesTTLExpr) // Write processed content to temp directory if err := os.WriteFile(destPath, []byte(processedContent), 0644); err != nil { @@ -345,9 +388,16 @@ func main() { } log.Println("Successfully connected to ClickHouse") - // Process schema directory (replace ${DATABASE} macro) + // Parse tables TTL + tablesTTLExpr, err := ttlToClickHouseInterval(cfg.TablesTTL) + if err != nil { + log.Fatalf("Invalid HYPERDX_OTEL_EXPORTER_TABLES_TTL: %v", err) + } + log.Printf("Tables TTL: %s (%s)", cfg.TablesTTL, tablesTTLExpr) + + // Process schema directory (replace ${DATABASE} and ${TABLES_TTL} macros) log.Printf("Preparing SQL files with database: %s", cfg.Database) - tempDir, err := processSchemaDir(cfg.SchemaDir, cfg.Database) + tempDir, err := processSchemaDir(cfg.SchemaDir, cfg.Database, tablesTTLExpr) if err != nil { log.Fatalf("Failed to process schema directory: %v", err) } diff --git a/packages/otel-collector/cmd/migrate/main_test.go b/packages/otel-collector/cmd/migrate/main_test.go new file mode 100644 index 00000000..a69f8879 --- /dev/null +++ b/packages/otel-collector/cmd/migrate/main_test.go @@ -0,0 +1,708 @@ +package main + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +// --------------------------------------------------------------------------- +// getEnv / getOrDefault +// --------------------------------------------------------------------------- + +func TestGetEnv(t *testing.T) { + const key = "TEST_GETENV_KEY_MIGRATE" + + // Returns default when env var is unset + got := getEnv(key, "fallback") + if got != "fallback" { + t.Errorf("getEnv unset: got %q, want %q", got, "fallback") + } + + // Returns value when env var is set + t.Setenv(key, "custom") + got = getEnv(key, "fallback") + if got != "custom" { + t.Errorf("getEnv set: got %q, want %q", got, "custom") + } + + // Treats empty string the same as unset (returns default) + t.Setenv(key, "") + got = getEnv(key, "fallback") + if got != "fallback" { + t.Errorf("getEnv empty: got %q, want %q", got, "fallback") + } +} + +func TestGetOrDefault(t *testing.T) { + if got := getOrDefault("", "def"); got != "def" { + t.Errorf("getOrDefault empty: got %q, want %q", got, "def") + } + if got := getOrDefault("val", "def"); got != "val" { + t.Errorf("getOrDefault non-empty: got %q, want %q", got, "val") + } +} + +// --------------------------------------------------------------------------- +// loadConfig +// --------------------------------------------------------------------------- + +func TestLoadConfig(t *testing.T) { + // Save and restore os.Args + origArgs := os.Args + t.Cleanup(func() { os.Args = origArgs }) + + t.Run("missing schema dir arg", func(t *testing.T) { + os.Args = []string{"migrate"} + _, err := loadConfig() + if err == nil { + t.Fatal("expected error when no schema dir argument") + } + if !strings.Contains(err.Error(), "usage:") { + t.Errorf("expected usage message, got: %v", err) + } + }) + + t.Run("nonexistent schema dir", func(t *testing.T) { + os.Args = []string{"migrate", "/nonexistent/path/schema"} + _, err := loadConfig() + if err == nil { + t.Fatal("expected error for nonexistent schema dir") + } + if !strings.Contains(err.Error(), "does not exist") { + t.Errorf("expected 'does not exist' error, got: %v", err) + } + }) + + t.Run("valid config with defaults", func(t *testing.T) { + dir := t.TempDir() + os.Args = []string{"migrate", dir} + + cfg, err := loadConfig() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.Endpoint != "tcp://localhost:9000" { + t.Errorf("Endpoint: got %q, want default", cfg.Endpoint) + } + if cfg.User != "default" { + t.Errorf("User: got %q, want %q", cfg.User, "default") + } + if cfg.Database != "default" { + t.Errorf("Database: got %q, want %q", cfg.Database, "default") + } + if cfg.TablesTTL != "720h" { + t.Errorf("TablesTTL: got %q, want %q", cfg.TablesTTL, "720h") + } + if cfg.SchemaDir != dir { + t.Errorf("SchemaDir: got %q, want %q", cfg.SchemaDir, dir) + } + if cfg.TLSInsecureSkipVerify { + t.Error("TLSInsecureSkipVerify should default to false") + } + }) + + t.Run("env var overrides", func(t *testing.T) { + dir := t.TempDir() + os.Args = []string{"migrate", dir} + t.Setenv("CLICKHOUSE_ENDPOINT", "https://ch.example.com:8443") + t.Setenv("CLICKHOUSE_USER", "admin") + t.Setenv("CLICKHOUSE_PASSWORD", "secret") + t.Setenv("HYPERDX_OTEL_EXPORTER_CLICKHOUSE_DATABASE", "mydb") + t.Setenv("HYPERDX_OTEL_EXPORTER_TABLES_TTL", "7d") + t.Setenv("CLICKHOUSE_TLS_INSECURE_SKIP_VERIFY", "true") + + cfg, err := loadConfig() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.Endpoint != "https://ch.example.com:8443" { + t.Errorf("Endpoint: got %q", cfg.Endpoint) + } + if cfg.User != "admin" { + t.Errorf("User: got %q", cfg.User) + } + if cfg.Password != "secret" { + t.Errorf("Password: got %q", cfg.Password) + } + if cfg.Database != "mydb" { + t.Errorf("Database: got %q", cfg.Database) + } + if cfg.TablesTTL != "7d" { + t.Errorf("TablesTTL: got %q", cfg.TablesTTL) + } + if !cfg.TLSInsecureSkipVerify { + t.Error("TLSInsecureSkipVerify should be true") + } + }) +} + +// --------------------------------------------------------------------------- +// parseEndpoint +// --------------------------------------------------------------------------- + +func TestParseEndpoint(t *testing.T) { + tests := []struct { + name string + input string + protocol string + host string + port string + secure bool + wantErr bool + }{ + { + name: "tcp with port", + input: "tcp://localhost:9000", + protocol: "native", host: "localhost", port: "9000", secure: false, + }, + { + name: "tcp default port", + input: "tcp://clickhouse", + protocol: "native", host: "clickhouse", port: "9000", secure: false, + }, + { + name: "http with port", + input: "http://clickhouse:8123", + protocol: "http", host: "clickhouse", port: "8123", secure: false, + }, + { + name: "http default port", + input: "http://clickhouse", + protocol: "http", host: "clickhouse", port: "8123", secure: false, + }, + { + name: "https with port", + input: "https://clickhouse:9443", + protocol: "http", host: "clickhouse", port: "9443", secure: true, + }, + { + name: "https default port", + input: "https://clickhouse.example.com", + protocol: "http", host: "clickhouse.example.com", port: "8443", secure: true, + }, + { + name: "no scheme defaults to tcp", + input: "clickhouse:9000", + protocol: "native", host: "clickhouse", port: "9000", secure: false, + }, + { + name: "unsupported scheme", + input: "ftp://clickhouse:21", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + proto, host, port, secure, err := parseEndpoint(tt.input) + if tt.wantErr { + if err == nil { + t.Errorf("parseEndpoint(%q) expected error", tt.input) + } + return + } + if err != nil { + t.Fatalf("parseEndpoint(%q) unexpected error: %v", tt.input, err) + } + if proto != tt.protocol { + t.Errorf("protocol: got %q, want %q", proto, tt.protocol) + } + if host != tt.host { + t.Errorf("host: got %q, want %q", host, tt.host) + } + if port != tt.port { + t.Errorf("port: got %q, want %q", port, tt.port) + } + if secure != tt.secure { + t.Errorf("secure: got %v, want %v", secure, tt.secure) + } + }) + } +} + +// --------------------------------------------------------------------------- +// parseTLSConfig +// --------------------------------------------------------------------------- + +// helper: generate a self-signed CA + leaf cert/key pair on disk. +func generateTestCerts(t *testing.T, dir string) (caFile, certFile, keyFile string) { + t.Helper() + + // CA key & cert + caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + caTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "test-ca"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + caDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey) + if err != nil { + t.Fatal(err) + } + caFile = filepath.Join(dir, "ca.pem") + if err := os.WriteFile(caFile, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caDER}), 0644); err != nil { + t.Fatal(err) + } + + // Leaf key & cert (signed by CA) + leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + leafTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{CommonName: "test-leaf"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + } + leafDER, err := x509.CreateCertificate(rand.Reader, leafTemplate, caTemplate, &leafKey.PublicKey, caKey) + if err != nil { + t.Fatal(err) + } + certFile = filepath.Join(dir, "cert.pem") + if err := os.WriteFile(certFile, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafDER}), 0644); err != nil { + t.Fatal(err) + } + + keyDER, err := x509.MarshalECPrivateKey(leafKey) + if err != nil { + t.Fatal(err) + } + keyFile = filepath.Join(dir, "key.pem") + if err := os.WriteFile(keyFile, pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}), 0644); err != nil { + t.Fatal(err) + } + + return caFile, certFile, keyFile +} + +func TestParseTLSConfig(t *testing.T) { + dir := t.TempDir() + caFile, certFile, keyFile := generateTestCerts(t, dir) + + t.Run("empty config", func(t *testing.T) { + cfg := &Config{} + tlsCfg, err := parseTLSConfig(cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tlsCfg.InsecureSkipVerify { + t.Error("InsecureSkipVerify should be false") + } + if tlsCfg.ServerName != "" { + t.Errorf("ServerName should be empty, got %q", tlsCfg.ServerName) + } + if tlsCfg.RootCAs != nil { + t.Error("RootCAs should be nil") + } + if len(tlsCfg.Certificates) != 0 { + t.Error("Certificates should be empty") + } + }) + + t.Run("insecure skip verify", func(t *testing.T) { + cfg := &Config{TLSInsecureSkipVerify: true} + tlsCfg, err := parseTLSConfig(cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !tlsCfg.InsecureSkipVerify { + t.Error("InsecureSkipVerify should be true") + } + }) + + t.Run("server name override", func(t *testing.T) { + cfg := &Config{TLSServerNameOverride: "custom.host"} + tlsCfg, err := parseTLSConfig(cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tlsCfg.ServerName != "custom.host" { + t.Errorf("ServerName: got %q, want %q", tlsCfg.ServerName, "custom.host") + } + }) + + t.Run("CA certificate", func(t *testing.T) { + cfg := &Config{TLSCAFile: caFile} + tlsCfg, err := parseTLSConfig(cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tlsCfg.RootCAs == nil { + t.Fatal("RootCAs should not be nil") + } + }) + + t.Run("client certificate", func(t *testing.T) { + cfg := &Config{TLSCertFile: certFile, TLSKeyFile: keyFile} + tlsCfg, err := parseTLSConfig(cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(tlsCfg.Certificates) != 1 { + t.Errorf("expected 1 client cert, got %d", len(tlsCfg.Certificates)) + } + }) + + t.Run("full TLS config", func(t *testing.T) { + cfg := &Config{ + TLSCAFile: caFile, + TLSCertFile: certFile, + TLSKeyFile: keyFile, + TLSServerNameOverride: "ch.example.com", + TLSInsecureSkipVerify: true, + } + tlsCfg, err := parseTLSConfig(cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tlsCfg.RootCAs == nil { + t.Error("RootCAs should not be nil") + } + if len(tlsCfg.Certificates) != 1 { + t.Errorf("expected 1 client cert, got %d", len(tlsCfg.Certificates)) + } + if tlsCfg.ServerName != "ch.example.com" { + t.Errorf("ServerName: got %q", tlsCfg.ServerName) + } + if !tlsCfg.InsecureSkipVerify { + t.Error("InsecureSkipVerify should be true") + } + }) + + t.Run("nonexistent CA file", func(t *testing.T) { + cfg := &Config{TLSCAFile: "/nonexistent/ca.pem"} + _, err := parseTLSConfig(cfg) + if err == nil { + t.Fatal("expected error for nonexistent CA file") + } + }) + + t.Run("invalid CA PEM", func(t *testing.T) { + badCA := filepath.Join(dir, "bad-ca.pem") + if err := os.WriteFile(badCA, []byte("not a certificate"), 0644); err != nil { + t.Fatal(err) + } + cfg := &Config{TLSCAFile: badCA} + _, err := parseTLSConfig(cfg) + if err == nil { + t.Fatal("expected error for invalid CA PEM") + } + }) + + t.Run("nonexistent client cert", func(t *testing.T) { + cfg := &Config{TLSCertFile: "/nonexistent/cert.pem", TLSKeyFile: keyFile} + _, err := parseTLSConfig(cfg) + if err == nil { + t.Fatal("expected error for nonexistent client cert") + } + }) + + t.Run("cert without key is ignored", func(t *testing.T) { + // Only TLSCertFile set, TLSKeyFile empty → should not load client cert + cfg := &Config{TLSCertFile: certFile} + tlsCfg, err := parseTLSConfig(cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(tlsCfg.Certificates) != 0 { + t.Error("should not load client cert when key is missing") + } + }) +} + +// --------------------------------------------------------------------------- +// listSQLFiles +// --------------------------------------------------------------------------- + +func TestListSQLFiles(t *testing.T) { + t.Run("mixed files", func(t *testing.T) { + dir := t.TempDir() + // Create SQL and non-SQL files + for _, name := range []string{"002_b.sql", "001_a.sql", "readme.md", "003_c.sql"} { + if err := os.WriteFile(filepath.Join(dir, name), []byte("--"), 0644); err != nil { + t.Fatal(err) + } + } + // Create a subdirectory (should be skipped) + if err := os.Mkdir(filepath.Join(dir, "subdir"), 0755); err != nil { + t.Fatal(err) + } + + files, err := listSQLFiles(dir) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expected := []string{"001_a.sql", "002_b.sql", "003_c.sql"} + if len(files) != len(expected) { + t.Fatalf("got %d files, want %d: %v", len(files), len(expected), files) + } + for i, f := range files { + if f != expected[i] { + t.Errorf("files[%d] = %q, want %q", i, f, expected[i]) + } + } + }) + + t.Run("empty dir", func(t *testing.T) { + dir := t.TempDir() + files, err := listSQLFiles(dir) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(files) != 0 { + t.Errorf("expected no files, got %v", files) + } + }) + + t.Run("nonexistent dir", func(t *testing.T) { + _, err := listSQLFiles("/nonexistent/dir") + if err == nil { + t.Fatal("expected error for nonexistent dir") + } + }) +} + +// --------------------------------------------------------------------------- +// processSchemaDir (additional cases) +// --------------------------------------------------------------------------- + +func TestProcessSchemaDir_Subdirectories(t *testing.T) { + schemaDir := t.TempDir() + subDir := filepath.Join(schemaDir, "sub") + if err := os.Mkdir(subDir, 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile( + filepath.Join(subDir, "001_nested.sql"), + []byte("CREATE DATABASE IF NOT EXISTS ${DATABASE};"), + 0644, + ); err != nil { + t.Fatal(err) + } + + tempDir, err := processSchemaDir(schemaDir, "testdb", "toIntervalHour(48)") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + got, err := os.ReadFile(filepath.Join(tempDir, "sub", "001_nested.sql")) + if err != nil { + t.Fatalf("nested file not found: %v", err) + } + expected := "CREATE DATABASE IF NOT EXISTS testdb;" + if string(got) != expected { + t.Errorf("got %q, want %q", string(got), expected) + } +} + +func TestProcessSchemaDir_MultipleReplacements(t *testing.T) { + schemaDir := t.TempDir() + content := "${DATABASE}.t1 TTL ${TABLES_TTL} ${DATABASE}.t2 TTL ${TABLES_TTL}" + if err := os.WriteFile(filepath.Join(schemaDir, "001.sql"), []byte(content), 0644); err != nil { + t.Fatal(err) + } + + tempDir, err := processSchemaDir(schemaDir, "db", "toIntervalDay(7)") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + got, err := os.ReadFile(filepath.Join(tempDir, "001.sql")) + if err != nil { + t.Fatal(err) + } + expected := "db.t1 TTL toIntervalDay(7) db.t2 TTL toIntervalDay(7)" + if string(got) != expected { + t.Errorf("got %q, want %q", string(got), expected) + } +} + +func TestProcessSchemaDir_NoMacros(t *testing.T) { + schemaDir := t.TempDir() + content := "SELECT 1;" + if err := os.WriteFile(filepath.Join(schemaDir, "001.sql"), []byte(content), 0644); err != nil { + t.Fatal(err) + } + + tempDir, err := processSchemaDir(schemaDir, "db", "toIntervalDay(30)") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + got, err := os.ReadFile(filepath.Join(tempDir, "001.sql")) + if err != nil { + t.Fatal(err) + } + if string(got) != content { + t.Errorf("content should be unchanged: got %q", string(got)) + } +} + +func TestProcessSchemaDir_NonexistentDir(t *testing.T) { + _, err := processSchemaDir("/nonexistent/schema", "db", "toIntervalDay(30)") + if err == nil { + t.Fatal("expected error for nonexistent schema dir") + } +} + +// --------------------------------------------------------------------------- +// parseTTLDuration / ttlToClickHouseInterval (existing tests below) +// --------------------------------------------------------------------------- + +func TestParseTTLDuration(t *testing.T) { + tests := []struct { + input string + expected time.Duration + wantErr bool + }{ + // Days + {"1d", 24 * time.Hour, false}, + {"30d", 30 * 24 * time.Hour, false}, + {"365d", 365 * 24 * time.Hour, false}, + + // Standard Go durations + {"720h", 720 * time.Hour, false}, + {"48h", 48 * time.Hour, false}, + {"90m", 90 * time.Minute, false}, + {"3600s", 3600 * time.Second, false}, + {"1h30m", time.Hour + 30*time.Minute, false}, + + // Errors + {"", 0, true}, + {"abc", 0, true}, + {"d", 0, true}, + {"12.5d", 0, true}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got, err := parseTTLDuration(tt.input) + if tt.wantErr { + if err == nil { + t.Errorf("parseTTLDuration(%q) expected error, got %v", tt.input, got) + } + return + } + if err != nil { + t.Errorf("parseTTLDuration(%q) unexpected error: %v", tt.input, err) + return + } + if got != tt.expected { + t.Errorf("parseTTLDuration(%q) = %v, want %v", tt.input, got, tt.expected) + } + }) + } +} + +func TestTTLToClickHouseInterval(t *testing.T) { + tests := []struct { + input string + expected string + wantErr bool + }{ + // Days - evenly divisible by 24h + {"30d", "toIntervalDay(30)", false}, + {"1d", "toIntervalDay(1)", false}, + {"365d", "toIntervalDay(365)", false}, + {"720h", "toIntervalDay(30)", false}, + {"48h", "toIntervalDay(2)", false}, + {"24h", "toIntervalDay(1)", false}, + + // Hours - not evenly divisible by 24h + {"36h", "toIntervalHour(36)", false}, + {"1h", "toIntervalHour(1)", false}, + {"100h", "toIntervalHour(100)", false}, + + // Minutes + {"90m", "toIntervalMinute(90)", false}, + {"30m", "toIntervalMinute(30)", false}, + {"1h30m", "toIntervalMinute(90)", false}, + + // Seconds + {"90s", "toIntervalSecond(90)", false}, + {"1m30s", "toIntervalSecond(90)", false}, + + // Errors + {"0s", "", true}, + {"-1h", "", true}, + {"abc", "", true}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got, err := ttlToClickHouseInterval(tt.input) + if tt.wantErr { + if err == nil { + t.Errorf("ttlToClickHouseInterval(%q) expected error, got %q", tt.input, got) + } + return + } + if err != nil { + t.Errorf("ttlToClickHouseInterval(%q) unexpected error: %v", tt.input, err) + return + } + if got != tt.expected { + t.Errorf("ttlToClickHouseInterval(%q) = %q, want %q", tt.input, got, tt.expected) + } + }) + } +} + +func TestProcessSchemaDir(t *testing.T) { + // Create a temp schema directory with a test SQL file + schemaDir := t.TempDir() + sqlContent := `CREATE TABLE IF NOT EXISTS ${DATABASE}.test_table +(col1 String) +ENGINE = MergeTree +ORDER BY col1 +TTL toDateTime(col1) + ${TABLES_TTL} +SETTINGS ttl_only_drop_parts = 1;` + + if err := os.WriteFile(filepath.Join(schemaDir, "001_test.sql"), []byte(sqlContent), 0644); err != nil { + t.Fatal(err) + } + + tempDir, err := processSchemaDir(schemaDir, "mydb", "toIntervalDay(30)") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + got, err := os.ReadFile(filepath.Join(tempDir, "001_test.sql")) + if err != nil { + t.Fatal(err) + } + + expected := `CREATE TABLE IF NOT EXISTS mydb.test_table +(col1 String) +ENGINE = MergeTree +ORDER BY col1 +TTL toDateTime(col1) + toIntervalDay(30) +SETTINGS ttl_only_drop_parts = 1;` + + if string(got) != expected { + t.Errorf("processSchemaDir output mismatch\ngot:\n%s\nwant:\n%s", string(got), expected) + } +} diff --git a/packages/otel-collector/go.mod b/packages/otel-collector/go.mod index f018e5cc..01fa990d 100644 --- a/packages/otel-collector/go.mod +++ b/packages/otel-collector/go.mod @@ -3,28 +3,28 @@ module github.com/hyperdxio/hyperdx/packages/otel-collector go 1.25 require ( - github.com/ClickHouse/clickhouse-go/v2 v2.30.0 - github.com/pressly/goose/v3 v3.24.1 + github.com/ClickHouse/clickhouse-go/v2 v2.43.0 + github.com/pressly/goose/v3 v3.26.0 ) require ( - github.com/ClickHouse/ch-go v0.61.5 // indirect - github.com/andybalholm/brotli v1.1.1 // indirect + github.com/ClickHouse/ch-go v0.71.0 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/go-faster/city v1.0.1 // indirect github.com/go-faster/errors v0.7.1 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/klauspost/compress v1.17.7 // indirect + github.com/klauspost/compress v1.18.3 // indirect github.com/mfridman/interpolate v0.0.2 // indirect - github.com/paulmach/orb v0.11.1 // indirect - github.com/pierrec/lz4/v4 v4.1.21 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/segmentio/asm v1.2.0 // indirect + github.com/paulmach/orb v0.12.0 // indirect + github.com/pierrec/lz4/v4 v4.1.25 // indirect + github.com/segmentio/asm v1.2.1 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect - go.opentelemetry.io/otel v1.26.0 // indirect - go.opentelemetry.io/otel/trace v1.26.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect ) diff --git a/packages/otel-collector/go.sum b/packages/otel-collector/go.sum index cc530261..f1acf5e0 100644 --- a/packages/otel-collector/go.sum +++ b/packages/otel-collector/go.sum @@ -1,9 +1,11 @@ -github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4= -github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg= -github.com/ClickHouse/clickhouse-go/v2 v2.30.0 h1:AG4D/hW39qa58+JHQIFOSnxyL46H6h2lrmGGk17dhFo= -github.com/ClickHouse/clickhouse-go/v2 v2.30.0/go.mod h1:i9ZQAojcayW3RsdCb3YR+n+wC2h65eJsZCscZ1Z1wyo= -github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= -github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM= +github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw= +github.com/ClickHouse/clickhouse-go/v2 v2.43.0 h1:fUR05TrF1GyvLDa/mAQjkx7KbgwdLRffs2n9O3WobtE= +github.com/ClickHouse/clickhouse-go/v2 v2.43.0/go.mod h1:o6jf7JM/zveWC/PP277BLxjHy5KjnGX/jfljhM4s34g= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -18,23 +20,21 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= -github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= +github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= @@ -42,31 +42,28 @@ github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGA github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= -github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= +github.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s= +github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= -github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= -github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0= +github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pressly/goose/v3 v3.24.1 h1:bZmxRco2uy5uu5Ng1MMVEfYsFlrMJI+e/VMXHQ3C4LY= -github.com/pressly/goose/v3 v3.24.1/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528DV+4DORug= +github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= +github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= -github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= @@ -77,16 +74,20 @@ github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7Jul github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= -go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= -go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= -go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= -go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -98,16 +99,16 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -124,23 +125,16 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= -modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= -modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= -modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= -modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= -modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= -modernc.org/sqlite v1.34.1 h1:u3Yi6M0N8t9yKRDwhXcyp1eS5/ErhPTBggxWFuR6Hfk= -modernc.org/sqlite v1.34.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k= -modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= -modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= +modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= +modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=