mirror of
https://github.com/hanzoai/otel-collector.git
synced 2026-11-03 02:20:04 +00:00
454 lines
12 KiB
Go
454 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"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/ClickHouse/clickhouse-go/v2"
|
|
"github.com/SigNoz/signoz-otel-collector/usage"
|
|
"go.opencensus.io/stats"
|
|
"go.opencensus.io/stats/view"
|
|
"go.opencensus.io/tag"
|
|
"go.opentelemetry.io/collector/component"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type Encoding string
|
|
|
|
const (
|
|
// EncodingJSON is used for spans encoded as JSON.
|
|
EncodingJSON Encoding = "json"
|
|
// EncodingProto is used for spans encoded as Protobuf.
|
|
EncodingProto Encoding = "protobuf"
|
|
)
|
|
|
|
// SpanWriter for writing spans to ClickHouse
|
|
type SpanWriter struct {
|
|
logger *zap.Logger
|
|
db clickhouse.Conn
|
|
traceDatabase string
|
|
indexTable string
|
|
errorTable string
|
|
spansTable string
|
|
attributeTable string
|
|
attributeKeyTable string
|
|
encoding Encoding
|
|
delay time.Duration
|
|
size int
|
|
spans chan *Span
|
|
finish chan bool
|
|
done sync.WaitGroup
|
|
}
|
|
|
|
type WriterOptions struct {
|
|
logger *zap.Logger
|
|
db clickhouse.Conn
|
|
traceDatabase string
|
|
spansTable string
|
|
indexTable string
|
|
errorTable string
|
|
attributeTable string
|
|
attributeKeyTable string
|
|
encoding Encoding
|
|
delay time.Duration
|
|
size int
|
|
}
|
|
|
|
// NewSpanWriter returns a SpanWriter for the database
|
|
func NewSpanWriter(options WriterOptions) *SpanWriter {
|
|
if err := view.Register(SpansCountView, SpansCountBytesView); err != nil {
|
|
return nil
|
|
}
|
|
writer := &SpanWriter{
|
|
logger: options.logger,
|
|
db: options.db,
|
|
traceDatabase: options.traceDatabase,
|
|
indexTable: options.indexTable,
|
|
errorTable: options.errorTable,
|
|
spansTable: options.spansTable,
|
|
attributeTable: options.attributeTable,
|
|
attributeKeyTable: options.attributeKeyTable,
|
|
encoding: options.encoding,
|
|
delay: options.delay,
|
|
size: options.size,
|
|
spans: make(chan *Span, options.size),
|
|
finish: make(chan bool),
|
|
}
|
|
|
|
go writer.backgroundWriter()
|
|
|
|
return writer
|
|
}
|
|
|
|
func (w *SpanWriter) backgroundWriter() {
|
|
batch := make([]*Span, 0, w.size)
|
|
|
|
timer := time.After(w.delay)
|
|
last := time.Now()
|
|
|
|
for {
|
|
w.done.Add(1)
|
|
|
|
flush := false
|
|
finish := false
|
|
|
|
select {
|
|
case span := <-w.spans:
|
|
batch = append(batch, span)
|
|
flush = len(batch) == cap(batch)
|
|
case <-timer:
|
|
timer = time.After(w.delay)
|
|
flush = time.Since(last) > w.delay && len(batch) > 0
|
|
case <-w.finish:
|
|
finish = true
|
|
flush = len(batch) > 0
|
|
}
|
|
|
|
if flush {
|
|
if err := w.writeBatch(batch); err != nil {
|
|
w.logger.Error("Could not write a batch of spans", zap.Error(err))
|
|
}
|
|
|
|
batch = make([]*Span, 0, w.size)
|
|
last = time.Now()
|
|
}
|
|
|
|
w.done.Done()
|
|
|
|
if finish {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func (w *SpanWriter) writeBatch(batch []*Span) error {
|
|
|
|
if w.spansTable != "" {
|
|
if err := w.writeModelBatch(batch); err != nil {
|
|
logBatch := batch[:int(math.Min(10, float64(len(batch))))]
|
|
w.logger.Error("Could not write a batch of spans to model table: ", zap.Any("batch", logBatch), zap.Error(err))
|
|
return err
|
|
}
|
|
}
|
|
if w.indexTable != "" {
|
|
if err := w.writeIndexBatch(batch); err != nil {
|
|
logBatch := batch[:int(math.Min(10, float64(len(batch))))]
|
|
w.logger.Error("Could not write a batch of spans to index table: ", zap.Any("batch", logBatch), zap.Error(err))
|
|
return err
|
|
}
|
|
}
|
|
if w.errorTable != "" {
|
|
if err := w.writeErrorBatch(batch); err != nil {
|
|
logBatch := batch[:int(math.Min(10, float64(len(batch))))]
|
|
w.logger.Error("Could not write a batch of spans to error table: ", zap.Any("batch", logBatch), zap.Error(err))
|
|
return err
|
|
}
|
|
}
|
|
if w.attributeTable != "" && w.attributeKeyTable != "" {
|
|
if err := w.writeTagBatch(batch); err != nil {
|
|
w.logger.Error("Could not write a batch of spans to tag/tagKey tables: ", zap.Error(err))
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w *SpanWriter) writeIndexBatch(batchSpans []*Span) error {
|
|
|
|
ctx := context.Background()
|
|
statement, err := w.db.PrepareBatch(ctx, fmt.Sprintf("INSERT INTO %s.%s", w.traceDatabase, w.indexTable))
|
|
if err != nil {
|
|
logBatch := batchSpans[:int(math.Min(10, float64(len(batchSpans))))]
|
|
w.logger.Error("Could not prepare batch for index table: ", zap.Any("batch", logBatch), zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
for _, span := range batchSpans {
|
|
err = statement.Append(
|
|
time.Unix(0, int64(span.StartTimeUnixNano)),
|
|
span.TraceId,
|
|
span.SpanId,
|
|
span.ParentSpanId,
|
|
span.ServiceName,
|
|
span.Name,
|
|
span.Kind,
|
|
span.DurationNano,
|
|
span.StatusCode,
|
|
span.ExternalHttpMethod,
|
|
span.ExternalHttpUrl,
|
|
span.Component,
|
|
span.DBSystem,
|
|
span.DBName,
|
|
span.DBOperation,
|
|
span.PeerService,
|
|
span.Events,
|
|
span.HttpMethod,
|
|
span.HttpUrl,
|
|
span.HttpCode,
|
|
span.HttpRoute,
|
|
span.HttpHost,
|
|
span.MsgSystem,
|
|
span.MsgOperation,
|
|
span.HasError,
|
|
span.TagMap,
|
|
span.GRPCMethod,
|
|
span.GRPCCode,
|
|
span.RPCSystem,
|
|
span.RPCService,
|
|
span.RPCMethod,
|
|
span.ResponseStatusCode,
|
|
span.StringTagMap,
|
|
span.NumberTagMap,
|
|
span.BoolTagMap,
|
|
span.ResourceTagsMap,
|
|
)
|
|
if err != nil {
|
|
w.logger.Error("Could not append span to batch: ", zap.Object("span", span), zap.Error(err))
|
|
return err
|
|
}
|
|
}
|
|
|
|
start := time.Now()
|
|
|
|
err = statement.Send()
|
|
|
|
ctx, _ = tag.New(ctx,
|
|
tag.Upsert(exporterKey, string(component.DataTypeTraces)),
|
|
tag.Upsert(tableKey, w.indexTable),
|
|
)
|
|
stats.Record(ctx, writeLatencyMillis.M(int64(time.Since(start).Milliseconds())))
|
|
return err
|
|
}
|
|
|
|
func (w *SpanWriter) writeTagBatch(batchSpans []*Span) error {
|
|
|
|
ctx := context.Background()
|
|
tagStatement, err := w.db.PrepareBatch(ctx, fmt.Sprintf("INSERT INTO %s.%s", w.traceDatabase, w.attributeTable))
|
|
if err != nil {
|
|
logBatch := batchSpans[:int(math.Min(10, float64(len(batchSpans))))]
|
|
w.logger.Error("Could not prepare batch for span attributes table due to error: ", zap.Error(err), zap.Any("batch", logBatch))
|
|
return err
|
|
}
|
|
tagKeyStatement, err := w.db.PrepareBatch(ctx, fmt.Sprintf("INSERT INTO %s.%s", w.traceDatabase, w.attributeKeyTable))
|
|
if err != nil {
|
|
logBatch := batchSpans[:int(math.Min(10, float64(len(batchSpans))))]
|
|
w.logger.Error("Could not prepare batch for span attributes key table due to error: ", zap.Error(err), zap.Any("batch", logBatch))
|
|
return err
|
|
}
|
|
for _, span := range batchSpans {
|
|
for _, spanAttribute := range span.SpanAttributes {
|
|
|
|
err = tagKeyStatement.Append(
|
|
spanAttribute.Key,
|
|
spanAttribute.TagType,
|
|
spanAttribute.DataType,
|
|
spanAttribute.IsColumn,
|
|
)
|
|
if err != nil {
|
|
w.logger.Error("Could not append span to tagKey Statement to batch due to error: ", zap.Error(err), zap.Object("span", span))
|
|
return err
|
|
}
|
|
|
|
if spanAttribute.DataType == "string" {
|
|
err = tagStatement.Append(
|
|
time.Unix(0, int64(span.StartTimeUnixNano)),
|
|
spanAttribute.Key,
|
|
spanAttribute.TagType,
|
|
spanAttribute.DataType,
|
|
spanAttribute.StringValue,
|
|
nil,
|
|
spanAttribute.IsColumn,
|
|
)
|
|
} else if spanAttribute.DataType == "float64" {
|
|
err = tagStatement.Append(
|
|
time.Unix(0, int64(span.StartTimeUnixNano)),
|
|
spanAttribute.Key,
|
|
spanAttribute.TagType,
|
|
spanAttribute.DataType,
|
|
nil,
|
|
spanAttribute.NumberValue,
|
|
spanAttribute.IsColumn,
|
|
)
|
|
} else if spanAttribute.DataType == "bool" {
|
|
err = tagStatement.Append(
|
|
time.Unix(0, int64(span.StartTimeUnixNano)),
|
|
spanAttribute.Key,
|
|
spanAttribute.TagType,
|
|
spanAttribute.DataType,
|
|
nil,
|
|
nil,
|
|
spanAttribute.IsColumn,
|
|
)
|
|
}
|
|
if err != nil {
|
|
w.logger.Error("Could not append span to tag Statement batch due to error: ", zap.Error(err), zap.Object("span", span))
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
tagStart := time.Now()
|
|
err = tagStatement.Send()
|
|
stats.RecordWithTags(ctx,
|
|
[]tag.Mutator{
|
|
tag.Upsert(exporterKey, string(component.DataTypeTraces)),
|
|
tag.Upsert(tableKey, w.attributeTable),
|
|
},
|
|
writeLatencyMillis.M(int64(time.Since(tagStart).Milliseconds())),
|
|
)
|
|
if err != nil {
|
|
logBatch := batchSpans[:int(math.Min(10, float64(len(batchSpans))))]
|
|
w.logger.Error("Could not write to span attributes table due to error: ", zap.Error(err), zap.Any("batch", logBatch))
|
|
return err
|
|
}
|
|
|
|
tagKeyStart := time.Now()
|
|
err = tagKeyStatement.Send()
|
|
stats.RecordWithTags(ctx,
|
|
[]tag.Mutator{
|
|
tag.Upsert(exporterKey, string(component.DataTypeTraces)),
|
|
tag.Upsert(tableKey, w.attributeKeyTable),
|
|
},
|
|
writeLatencyMillis.M(int64(time.Since(tagKeyStart).Milliseconds())),
|
|
)
|
|
if err != nil {
|
|
logBatch := batchSpans[:int(math.Min(10, float64(len(batchSpans))))]
|
|
w.logger.Error("Could not write to span attributes key table due to error: ", zap.Error(err), zap.Any("batch", logBatch))
|
|
return err
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (w *SpanWriter) writeErrorBatch(batchSpans []*Span) error {
|
|
|
|
ctx := context.Background()
|
|
statement, err := w.db.PrepareBatch(ctx, fmt.Sprintf("INSERT INTO %s.%s", w.traceDatabase, w.errorTable))
|
|
if err != nil {
|
|
logBatch := batchSpans[:int(math.Min(10, float64(len(batchSpans))))]
|
|
w.logger.Error("Could not prepare batch for error table: ", zap.Any("batch", logBatch), zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
for _, span := range batchSpans {
|
|
if span.ErrorEvent.Name == "" {
|
|
continue
|
|
}
|
|
err = statement.Append(
|
|
time.Unix(0, int64(span.ErrorEvent.TimeUnixNano)),
|
|
span.ErrorID,
|
|
span.ErrorGroupID,
|
|
span.TraceId,
|
|
span.SpanId,
|
|
span.ServiceName,
|
|
span.ErrorEvent.AttributeMap["exception.type"],
|
|
span.ErrorEvent.AttributeMap["exception.message"],
|
|
span.ErrorEvent.AttributeMap["exception.stacktrace"],
|
|
stringToBool(span.ErrorEvent.AttributeMap["exception.escaped"]),
|
|
span.ResourceTagsMap,
|
|
)
|
|
if err != nil {
|
|
w.logger.Error("Could not append span to batch: ", zap.Object("span", span), zap.Error(err))
|
|
return err
|
|
}
|
|
}
|
|
|
|
start := time.Now()
|
|
|
|
err = statement.Send()
|
|
|
|
ctx, _ = tag.New(ctx,
|
|
tag.Upsert(exporterKey, string(component.DataTypeTraces)),
|
|
tag.Upsert(tableKey, w.errorTable),
|
|
)
|
|
stats.Record(ctx, writeLatencyMillis.M(int64(time.Since(start).Milliseconds())))
|
|
return err
|
|
}
|
|
|
|
func stringToBool(s string) bool {
|
|
if strings.ToLower(s) == "true" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (w *SpanWriter) writeModelBatch(batchSpans []*Span) error {
|
|
ctx := context.Background()
|
|
statement, err := w.db.PrepareBatch(ctx, fmt.Sprintf("INSERT INTO %s.%s", w.traceDatabase, w.spansTable))
|
|
if err != nil {
|
|
logBatch := batchSpans[:int(math.Min(10, float64(len(batchSpans))))]
|
|
w.logger.Error("Could not prepare batch for model table: ", zap.Any("batch", logBatch), zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
metrics := map[string]usage.Metric{}
|
|
for _, span := range batchSpans {
|
|
var serialized []byte
|
|
usageMap := span.TraceModel
|
|
usageMap.TagMap = map[string]string{}
|
|
serialized, err = json.Marshal(span.TraceModel)
|
|
serializedUsage, err := json.Marshal(usageMap)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = statement.Append(time.Unix(0, int64(span.StartTimeUnixNano)), span.TraceId, string(serialized))
|
|
if err != nil {
|
|
w.logger.Error("Could not append span to batch: ", zap.Object("span", span), zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
usage.AddMetric(metrics, *span.Tenant, 1, int64(len(serializedUsage)))
|
|
}
|
|
start := time.Now()
|
|
|
|
err = statement.Send()
|
|
ctx, _ = tag.New(ctx,
|
|
tag.Upsert(exporterKey, string(component.DataTypeTraces)),
|
|
tag.Upsert(tableKey, w.spansTable),
|
|
)
|
|
stats.Record(ctx, writeLatencyMillis.M(int64(time.Since(start).Milliseconds())))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for k, v := range metrics {
|
|
stats.RecordWithTags(ctx, []tag.Mutator{tag.Upsert(usage.TagTenantKey, k)}, ExporterSigNozSentSpans.M(int64(v.Count)), ExporterSigNozSentSpansBytes.M(int64(v.Size)))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// WriteSpan writes the encoded span
|
|
func (w *SpanWriter) WriteSpan(span *Span) error {
|
|
w.spans <- span
|
|
return nil
|
|
}
|
|
|
|
// Close Implements io.Closer and closes the underlying storage
|
|
func (w *SpanWriter) Close() error {
|
|
w.finish <- true
|
|
w.done.Wait()
|
|
return nil
|
|
}
|