Ctrl K
ComunidadGuías…

internal/clickhouse/clickhouse.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package clickhouse

import (
	"context"
	"fmt"

	clickhouse_go "github.com/ClickHouse/clickhouse-go/v2"
	"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
	"github.com/flexprice/flexprice/internal/config"
	"github.com/flexprice/flexprice/internal/tracing"
)

type ClickHouseStore struct {
	conn    driver.Conn
	tracing *tracing.Service
}

func NewClickHouseStore(config *config.Configuration, tracingService *tracing.Service) (*ClickHouseStore, error) {
	options := config.ClickHouse.GetClientOptions()
	conn, err := clickhouse_go.Open(options)
	if err != nil {
		return nil, fmt.Errorf("init clickhouse client: %w", err)
	}

	return &ClickHouseStore{
		conn:    conn,
		tracing: tracingService,
	}, nil
}

// GetConn returns the ClickHouse driver connection.
//
// When otel.traces.storage_spans_enabled is true (env:
// FLEXPRICE_OTEL_TRACES_STORAGE_SPANS_ENABLED=true), the connection is wrapped
// in tracedConn so every Select/QueryRow/Ping/Batch call emits a child span.
// The flag defaults to false to avoid span volume explosion before operators
// have a feel for the cost at scale.
func (s *ClickHouseStore) GetConn() driver.Conn {
	if s.tracing != nil && s.tracing.IsStorageSpansEnabled() {
		return &tracedConn{Conn: s.conn, tracing: s.tracing}
	}
	return s.conn
}

// Original connection accessor if needed
func (s *ClickHouseStore) GetRawConn() driver.Conn {
	return s.conn
}

func (s *ClickHouseStore) Close() error {
	return s.conn.Close()
}

// WithSpan creates a new context with a ClickHouse span for monitoring database operations
func (s *ClickHouseStore) WithSpan(ctx context.Context, operation string, params map[string]interface{}) (context.Context, *tracing.SpanFinisher) {
	if s.tracing == nil {
		return ctx, &tracing.SpanFinisher{}
	}

	span, newCtx := s.tracing.StartClickHouseSpan(ctx, operation, params)
	return newCtx, &tracing.SpanFinisher{Span: span}
}

// tracedConn wraps a ClickHouse connection with tracing.
type tracedConn struct {
	driver.Conn
	tracing *tracing.Service
}

// Select adds tracing and delegates to the underlying connection
func (tc *tracedConn) Select(ctx context.Context, dest any, query string, args ...any) error {
	if tc.tracing == nil {
		return tc.Conn.Select(ctx, dest, query, args...)
	}

	span, ctx := tc.tracing.StartClickHouseSpan(ctx, "clickhouse.select", map[string]interface{}{
		"db.statement": truncateQuery(query),
		"args_count":   len(args),
	})
	defer span.Finish()

	err := tc.Conn.Select(ctx, dest, query, args...)
	span.SetStatusError(err)
	return err
}

// Query adds tracing and delegates to the underlying connection
func (tc *tracedConn) Query(ctx context.Context, query string, args ...any) (driver.Rows, error) {
	if tc.tracing == nil {
		return tc.Conn.Query(ctx, query, args...)
	}

	// Note: the span covers the query call, not consumption of the returned
	// rows, so it measures time-to-first-response rather than full read time.
	span, ctx := tc.tracing.StartClickHouseSpan(ctx, "clickhouse.query", map[string]interface{}{
		"db.statement": truncateQuery(query),
		"args_count":   len(args),
	})
	defer span.Finish()

	rows, err := tc.Conn.Query(ctx, query, args...)
	span.SetStatusError(err)
	return rows, err
}

// QueryRow adds tracing and delegates to the underlying connection
func (tc *tracedConn) QueryRow(ctx context.Context, query string, args ...any) driver.Row {
	if tc.tracing == nil {
		return tc.Conn.QueryRow(ctx, query, args...)
	}

	span, ctx := tc.tracing.StartClickHouseSpan(ctx, "clickhouse.query_row", map[string]interface{}{
		"db.statement": truncateQuery(query),
		"args_count":   len(args),
	})
	defer span.Finish()

	return tc.Conn.QueryRow(ctx, query, args...)
}

// PrepareBatch adds tracing and delegates to the underlying connection
func (tc *tracedConn) PrepareBatch(ctx context.Context, query string, options ...driver.PrepareBatchOption) (driver.Batch, error) {
	if tc.tracing == nil {
		return tc.Conn.PrepareBatch(ctx, query, options...)
	}

	span, ctx := tc.tracing.StartClickHouseSpan(ctx, "clickhouse.prepare_batch", map[string]interface{}{
		"db.statement": truncateQuery(query),
	})
	defer span.Finish()

	batch, err := tc.Conn.PrepareBatch(ctx, query, options...)
	if err != nil {
		span.SetStatusError(err)
		return nil, err
	}

	return &tracedBatch{
		Batch:   batch,
		tracing: tc.tracing,
		ctx:     ctx,
	}, nil
}

// Exec adds tracing and delegates to the underlying connection
func (tc *tracedConn) Exec(ctx context.Context, query string, args ...any) error {
	if tc.tracing == nil {
		return tc.Conn.Exec(ctx, query, args...)
	}

	span, ctx := tc.tracing.StartClickHouseSpan(ctx, "clickhouse.exec", map[string]interface{}{
		"db.statement": truncateQuery(query),
		"args_count":   len(args),
	})
	defer span.Finish()

	err := tc.Conn.Exec(ctx, query, args...)
	span.SetStatusError(err)
	return err
}

// AsyncInsert adds tracing and delegates to the underlying connection
func (tc *tracedConn) AsyncInsert(ctx context.Context, query string, wait bool, args ...any) error {
	if tc.tracing == nil {
		return tc.Conn.AsyncInsert(ctx, query, wait, args...)
	}

	span, ctx := tc.tracing.StartClickHouseSpan(ctx, "clickhouse.async_insert", map[string]interface{}{
		"db.statement": truncateQuery(query),
		"wait":         wait,
	})
	defer span.Finish()

	err := tc.Conn.AsyncInsert(ctx, query, wait, args...)
	span.SetStatusError(err)
	return err
}

// Ping adds tracing and delegates to the underlying connection
func (tc *tracedConn) Ping(ctx context.Context) error {
	if tc.tracing == nil {
		return tc.Conn.Ping(ctx)
	}

	span, ctx := tc.tracing.StartClickHouseSpan(ctx, "clickhouse.ping", nil)
	defer span.Finish()

	err := tc.Conn.Ping(ctx)
	span.SetStatusError(err)
	return err
}

// tracedBatch wraps a ClickHouse batch with tracing.
type tracedBatch struct {
	driver.Batch
	tracing *tracing.Service
	ctx     context.Context // context captured at PrepareBatch time for child spans
}

// Flush delegates to the underlying batch
func (tb *tracedBatch) Flush() error {
	if tb.tracing == nil {
		return tb.Batch.Flush()
	}

	// Use the context captured at PrepareBatch time so the flush span is a
	// child of the originating request trace rather than a detached root span.
	span, _ := tb.tracing.StartClickHouseSpan(tb.ctx, "clickhouse.batch_flush", nil)
	defer span.Finish()

	err := tb.Batch.Flush()
	span.SetStatusError(err)
	return err
}

// Send adds tracing and delegates to the underlying batch
func (tb *tracedBatch) Send() error {
	if tb.tracing == nil {
		return tb.Batch.Send()
	}

	// Use the context captured at PrepareBatch time so the send span is a
	// child of the originating request trace rather than a detached root span.
	span, _ := tb.tracing.StartClickHouseSpan(tb.ctx, "clickhouse.batch_send", map[string]interface{}{
		"rows": tb.Batch.Rows(),
	})
	defer span.Finish()

	err := tb.Batch.Send()
	span.SetStatusError(err)
	return err
}

// truncateQuery limits query length to keep OTel span attributes small.
func truncateQuery(query string) string {
	const maxQueryLength = 1000
	if len(query) > maxQueryLength {
		return query[:maxQueryLength] + "..."
	}
	return query
}