mirror of
https://github.com/hanzoai/otel-collector.git
synced 2026-11-03 02:20:04 +00:00
332 lines
12 KiB
Go
332 lines
12 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package clickhousetracesexporter
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"net/url"
|
|
|
|
"github.com/ClickHouse/clickhouse-go/v2"
|
|
"github.com/golang-migrate/migrate/v4"
|
|
_ "github.com/golang-migrate/migrate/v4/database/clickhouse"
|
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
|
"github.com/spf13/viper"
|
|
"go.opencensus.io/stats"
|
|
"go.opencensus.io/stats/view"
|
|
"go.opencensus.io/tag"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Factory implements storage.Factory for Clickhouse backend.
|
|
type Factory struct {
|
|
logger *zap.Logger
|
|
Options *Options
|
|
db clickhouse.Conn
|
|
archive clickhouse.Conn
|
|
datasource string
|
|
makeWriter writerMaker
|
|
}
|
|
|
|
// Writer writes spans to storage.
|
|
type Writer interface {
|
|
WriteSpan(span *Span) error
|
|
}
|
|
|
|
type writerMaker func(WriterOptions) (Writer, error)
|
|
|
|
var (
|
|
writeLatencyMillis = stats.Int64("exporter_db_write_latency", "Time taken (in millis) for exporter to write batch", "ms")
|
|
exporterKey = tag.MustNewKey("exporter")
|
|
tableKey = tag.MustNewKey("table")
|
|
)
|
|
|
|
// NewFactory creates a new Factory.
|
|
func ClickHouseNewFactory(migrations string, datasource string, dockerMultiNodeCluster bool) *Factory {
|
|
writeLatencyDistribution := view.Distribution(100, 250, 500, 750, 1000, 2000, 4000, 8000, 16000, 32000, 64000, 128000, 256000, 512000)
|
|
|
|
writeLatencyView := &view.View{
|
|
Name: "exporter_db_write_latency",
|
|
Measure: writeLatencyMillis,
|
|
Description: writeLatencyMillis.Description(),
|
|
TagKeys: []tag.Key{exporterKey, tableKey},
|
|
Aggregation: writeLatencyDistribution,
|
|
}
|
|
|
|
view.Register(writeLatencyView)
|
|
return &Factory{
|
|
Options: NewOptions(migrations, datasource, dockerMultiNodeCluster, primaryNamespace, archiveNamespace),
|
|
// makeReader: func(db *clickhouse.Conn, operationsTable, indexTable, spansTable string) (spanstore.Reader, error) {
|
|
// return store.NewTraceReader(db, operationsTable, indexTable, spansTable), nil
|
|
// },
|
|
makeWriter: func(options WriterOptions) (Writer, error) {
|
|
return NewSpanWriter(options), nil
|
|
},
|
|
}
|
|
}
|
|
|
|
// Initialize implements storage.Factory
|
|
func (f *Factory) Initialize(logger *zap.Logger) error {
|
|
f.logger = logger
|
|
|
|
db, err := f.connect(f.Options.getPrimary())
|
|
if err != nil {
|
|
return fmt.Errorf("error connecting to primary db: %v", err)
|
|
}
|
|
|
|
f.db = db
|
|
|
|
archiveConfig := f.Options.others[archiveNamespace]
|
|
if archiveConfig.Enabled {
|
|
archive, err := f.connect(archiveConfig)
|
|
if err != nil {
|
|
return fmt.Errorf("error connecting to archive db: %v", err)
|
|
}
|
|
|
|
f.archive = archive
|
|
}
|
|
|
|
err = patchGroupByParenInMV(db, f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// drop schema migrations table if running in docker multi node cluster mode so that migrations are run on new nodes
|
|
if f.Options.primary.DockerMultiNodeCluster {
|
|
err = dropSchemaMigrationsTable(db, f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
f.logger.Info("Running migrations from path: ", zap.Any("test", f.Options.primary.Migrations))
|
|
clickhouseUrl, err := buildClickhouseMigrateURL(f.Options.primary.Datasource, f.Options.primary.Cluster)
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to build Clickhouse migrate URL, error: %s", err)
|
|
}
|
|
m, err := migrate.New(
|
|
"file://"+f.Options.primary.Migrations,
|
|
clickhouseUrl)
|
|
if err != nil {
|
|
return fmt.Errorf("Clickhouse Migrate failed to run, error: %s", err)
|
|
}
|
|
err = m.Up()
|
|
f.logger.Info("Clickhouse Migrate finished", zap.Error(err))
|
|
return nil
|
|
}
|
|
|
|
func patchGroupByParenInMV(db clickhouse.Conn, f *Factory) error {
|
|
|
|
// check if views already exist, if not, skip as patch is not required for fresh install
|
|
for _, table := range []string{f.Options.getPrimary().DependencyGraphDbMV, f.Options.getPrimary().DependencyGraphServiceMV, f.Options.getPrimary().DependencyGraphMessagingMV} {
|
|
var exists uint8
|
|
err := db.QueryRow(context.Background(), fmt.Sprintf("EXISTS VIEW %s.%s", f.Options.getPrimary().TraceDatabase, table)).Scan(&exists)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if exists == 0 {
|
|
f.logger.Info("View does not exist, skipping patch", zap.String("table", table))
|
|
return nil
|
|
}
|
|
}
|
|
f.logger.Info("Patching views")
|
|
// drop views
|
|
for _, table := range []string{f.Options.getPrimary().DependencyGraphDbMV, f.Options.getPrimary().DependencyGraphServiceMV, f.Options.getPrimary().DependencyGraphMessagingMV} {
|
|
err := db.Exec(context.Background(), fmt.Sprintf("DROP VIEW IF EXISTS %s.%s ON CLUSTER %s", f.Options.getPrimary().TraceDatabase, table, f.Options.getPrimary().Cluster))
|
|
if err != nil {
|
|
f.logger.Error(fmt.Sprintf("Error dropping %s view", table), zap.Error(err))
|
|
return fmt.Errorf("error dropping %s view: %v", table, err)
|
|
}
|
|
}
|
|
|
|
// create views with patched group by
|
|
err := db.Exec(context.Background(), fmt.Sprintf(`CREATE MATERIALIZED VIEW IF NOT EXISTS %s.%s ON CLUSTER %s
|
|
TO %s.%s AS
|
|
SELECT
|
|
A.serviceName as src,
|
|
B.serviceName as dest,
|
|
quantilesState(0.5, 0.75, 0.9, 0.95, 0.99)(toFloat64(B.durationNano)) as duration_quantiles_state,
|
|
countIf(B.statusCode=2) as error_count,
|
|
count(*) as total_count,
|
|
toStartOfMinute(B.timestamp) as timestamp
|
|
FROM %s.%s AS A, %s.%s AS B
|
|
WHERE (A.serviceName != B.serviceName) AND (A.spanID = B.parentSpanID)
|
|
GROUP BY timestamp, src, dest;`, f.Options.getPrimary().TraceDatabase, f.Options.getPrimary().DependencyGraphServiceMV,
|
|
f.Options.getPrimary().Cluster, f.Options.getPrimary().TraceDatabase, f.Options.getPrimary().DependencyGraphTable,
|
|
f.Options.getPrimary().TraceDatabase, f.Options.getPrimary().LocalIndexTable, f.Options.getPrimary().TraceDatabase,
|
|
f.Options.getPrimary().LocalIndexTable))
|
|
if err != nil {
|
|
f.logger.Error("Error creating "+f.Options.getPrimary().DependencyGraphServiceMV, zap.Error(err))
|
|
return fmt.Errorf("error creating %s: %v", f.Options.getPrimary().DependencyGraphServiceMV, err)
|
|
}
|
|
err = db.Exec(context.Background(), fmt.Sprintf(`CREATE MATERIALIZED VIEW IF NOT EXISTS %s.%s ON CLUSTER %s
|
|
TO %s.%s AS
|
|
SELECT
|
|
serviceName as src,
|
|
tagMap['db.system'] as dest,
|
|
quantilesState(0.5, 0.75, 0.9, 0.95, 0.99)(toFloat64(durationNano)) as duration_quantiles_state,
|
|
countIf(statusCode=2) as error_count,
|
|
count(*) as total_count,
|
|
toStartOfMinute(timestamp) as timestamp
|
|
FROM %s.%s
|
|
WHERE dest != '' and kind != 2
|
|
GROUP BY timestamp, src, dest;`, f.Options.getPrimary().TraceDatabase, f.Options.getPrimary().DependencyGraphDbMV,
|
|
f.Options.getPrimary().Cluster, f.Options.getPrimary().TraceDatabase, f.Options.getPrimary().DependencyGraphTable,
|
|
f.Options.getPrimary().TraceDatabase, f.Options.getPrimary().LocalIndexTable))
|
|
if err != nil {
|
|
f.logger.Error("Error creating "+f.Options.getPrimary().DependencyGraphDbMV, zap.Error(err))
|
|
return fmt.Errorf("error creating %s: %v", f.Options.getPrimary().DependencyGraphDbMV, err)
|
|
}
|
|
err = db.Exec(context.Background(), fmt.Sprintf(`CREATE MATERIALIZED VIEW IF NOT EXISTS %s.%s ON CLUSTER %s
|
|
TO %s.%s AS
|
|
SELECT
|
|
serviceName as src,
|
|
tagMap['messaging.system'] as dest,
|
|
quantilesState(0.5, 0.75, 0.9, 0.95, 0.99)(toFloat64(durationNano)) as duration_quantiles_state,
|
|
countIf(statusCode=2) as error_count,
|
|
count(*) as total_count,
|
|
toStartOfMinute(timestamp) as timestamp
|
|
FROM %s.%s
|
|
WHERE dest != '' and kind != 2
|
|
GROUP BY timestamp, src, dest;`, f.Options.getPrimary().TraceDatabase, f.Options.getPrimary().DependencyGraphMessagingMV,
|
|
f.Options.getPrimary().Cluster, f.Options.getPrimary().TraceDatabase, f.Options.getPrimary().DependencyGraphTable,
|
|
f.Options.getPrimary().TraceDatabase, f.Options.getPrimary().LocalIndexTable))
|
|
if err != nil {
|
|
f.logger.Error("Error creating "+f.Options.getPrimary().DependencyGraphMessagingMV, zap.Error(err))
|
|
return fmt.Errorf("error creating %s: %v", f.Options.getPrimary().DependencyGraphMessagingMV, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func dropSchemaMigrationsTable(db clickhouse.Conn, f *Factory) error {
|
|
err := db.Exec(context.Background(), fmt.Sprintf(`DROP TABLE IF EXISTS %s.%s ON CLUSTER %s;`,
|
|
f.Options.getPrimary().TraceDatabase, "schema_migrations", f.Options.getPrimary().Cluster))
|
|
if err != nil {
|
|
f.logger.Error("Error dropping schema_migrations table", zap.Error(err))
|
|
return fmt.Errorf("error dropping schema_migrations table: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func buildClickhouseMigrateURL(datasource string, cluster string) (string, error) {
|
|
// return fmt.Sprintf("clickhouse://localhost:9000?database=default&x-multi-statement=true"), nil
|
|
var clickhouseUrl string
|
|
database := "signoz_traces"
|
|
parsedURL, err := url.Parse(datasource)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
host := parsedURL.Host
|
|
if host == "" {
|
|
return "", fmt.Errorf("Unable to parse host")
|
|
|
|
}
|
|
paramMap, err := url.ParseQuery(parsedURL.RawQuery)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
username := paramMap["username"]
|
|
password := paramMap["password"]
|
|
|
|
if len(username) > 0 && len(password) > 0 {
|
|
clickhouseUrl = fmt.Sprintf("clickhouse://%s:%s@%s/%s?x-multi-statement=true&x-cluster-name=%s&x-migrations-table=schema_migrations&x-migrations-table-engine=MergeTree", username[0], password[0], host, database, cluster)
|
|
} else {
|
|
clickhouseUrl = fmt.Sprintf("clickhouse://%s/%s?x-multi-statement=true&x-cluster-name=%s&x-migrations-table=schema_migrations&x-migrations-table-engine=MergeTree", host, database, cluster)
|
|
}
|
|
return clickhouseUrl, nil
|
|
}
|
|
|
|
func (f *Factory) connect(cfg *namespaceConfig) (clickhouse.Conn, error) {
|
|
if cfg.Encoding != EncodingJSON && cfg.Encoding != EncodingProto {
|
|
return nil, fmt.Errorf("unknown encoding %q, supported: %q, %q", cfg.Encoding, EncodingJSON, EncodingProto)
|
|
}
|
|
|
|
return cfg.Connector(cfg)
|
|
}
|
|
|
|
// AddFlags implements plugin.Configurable
|
|
func (f *Factory) AddFlags(flagSet *flag.FlagSet) {
|
|
f.Options.AddFlags(flagSet)
|
|
}
|
|
|
|
// InitFromViper implements plugin.Configurable
|
|
func (f *Factory) InitFromViper(v *viper.Viper) {
|
|
f.Options.InitFromViper(v)
|
|
}
|
|
|
|
// CreateSpanWriter implements storage.Factory
|
|
func (f *Factory) CreateSpanWriter() (Writer, error) {
|
|
cfg := f.Options.getPrimary()
|
|
return f.makeWriter(WriterOptions{
|
|
logger: f.logger,
|
|
db: f.db,
|
|
traceDatabase: cfg.TraceDatabase,
|
|
spansTable: cfg.SpansTable,
|
|
indexTable: cfg.IndexTable,
|
|
errorTable: cfg.ErrorTable,
|
|
attributeTable: cfg.AttributeTable,
|
|
attributeKeyTable: cfg.AttributeKeyTable,
|
|
encoding: cfg.Encoding,
|
|
delay: cfg.WriteBatchDelay,
|
|
size: cfg.WriteBatchSize,
|
|
})
|
|
}
|
|
|
|
// CreateArchiveSpanWriter implements storage.ArchiveFactory
|
|
func (f *Factory) CreateArchiveSpanWriter() (Writer, error) {
|
|
if f.archive == nil {
|
|
return nil, nil
|
|
}
|
|
cfg := f.Options.others[archiveNamespace]
|
|
return f.makeWriter(WriterOptions{
|
|
logger: f.logger,
|
|
db: f.archive,
|
|
traceDatabase: cfg.TraceDatabase,
|
|
spansTable: cfg.SpansTable,
|
|
indexTable: cfg.IndexTable,
|
|
errorTable: cfg.ErrorTable,
|
|
attributeTable: cfg.AttributeTable,
|
|
attributeKeyTable: cfg.AttributeKeyTable,
|
|
encoding: cfg.Encoding,
|
|
delay: cfg.WriteBatchDelay,
|
|
size: cfg.WriteBatchSize,
|
|
})
|
|
}
|
|
|
|
// Close Implements io.Closer and closes the underlying storage
|
|
func (f *Factory) Close() error {
|
|
if f.db != nil {
|
|
err := f.db.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
f.db = nil
|
|
}
|
|
|
|
if f.archive != nil {
|
|
err := f.archive.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
f.archive = nil
|
|
}
|
|
|
|
return nil
|
|
}
|