mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
* Update live store to honor local and global block configs * changelog * Update config manifest * Reorder config validation and fix tests * lint
522 lines
15 KiB
Go
522 lines
15 KiB
Go
package livestore
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/hex"
|
|
"errors"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/go-kit/log"
|
|
"github.com/go-kit/log/level"
|
|
"github.com/google/uuid"
|
|
"github.com/grafana/tempo/modules/ingester"
|
|
"github.com/grafana/tempo/modules/overrides"
|
|
"github.com/grafana/tempo/pkg/livetraces"
|
|
"github.com/grafana/tempo/pkg/model"
|
|
"github.com/grafana/tempo/pkg/tempopb"
|
|
v1 "github.com/grafana/tempo/pkg/tempopb/trace/v1"
|
|
"github.com/grafana/tempo/pkg/tracesizes"
|
|
util_log "github.com/grafana/tempo/pkg/util/log"
|
|
"github.com/grafana/tempo/tempodb/backend"
|
|
"github.com/grafana/tempo/tempodb/encoding"
|
|
"github.com/grafana/tempo/tempodb/encoding/common"
|
|
"github.com/grafana/tempo/tempodb/wal"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
|
|
"go.opentelemetry.io/otel/attribute"
|
|
oteltrace "go.opentelemetry.io/otel/trace"
|
|
)
|
|
|
|
const (
|
|
traceDataType = "trace"
|
|
reasonWaitingForLiveTraces = "waiting_for_live_traces"
|
|
reasonWaitingForWAL = "waiting_for_wal"
|
|
maxTraceLogLinesPerSecond = 10
|
|
)
|
|
|
|
var (
|
|
// Instance-level metrics (similar to ingester instance.go)
|
|
metricTracesCreatedTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
|
Namespace: "tempo_live_store",
|
|
Name: "traces_created_total",
|
|
Help: "The total number of traces created per tenant.",
|
|
}, []string{"tenant"})
|
|
metricLiveTraces = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
|
Namespace: "tempo_live_store",
|
|
Name: "live_traces",
|
|
Help: "The current number of live traces per tenant.",
|
|
}, []string{"tenant"})
|
|
metricLiveTraceBytes = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
|
Namespace: "tempo_live_store",
|
|
Name: "live_trace_bytes",
|
|
Help: "The current number of bytes consumed by live traces per tenant.",
|
|
}, []string{"tenant"})
|
|
metricBytesReceivedTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
|
Namespace: "tempo_live_store",
|
|
Name: "bytes_received_total",
|
|
Help: "The total bytes received per tenant.",
|
|
}, []string{"tenant", "data_type"})
|
|
metricBlocksClearedTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
|
Namespace: "tempo_live_store",
|
|
Name: "blocks_cleared_total",
|
|
Help: "The total number of blocks cleared.",
|
|
}, []string{"block_type"})
|
|
metricCompletionSize = promauto.NewHistogram(prometheus.HistogramOpts{
|
|
Namespace: "tempo_live_store",
|
|
Name: "completion_size_bytes",
|
|
Help: "Size in bytes of blocks completed.",
|
|
Buckets: prometheus.ExponentialBuckets(1024*1024, 2, 10), // from 1MB up to 1GB
|
|
})
|
|
metricBackPressure = promauto.NewCounterVec(prometheus.CounterOpts{
|
|
Namespace: "tempo",
|
|
Subsystem: "live_store",
|
|
Name: "back_pressure_seconds_total",
|
|
Help: "The total amount of time spent waiting to process data from queue",
|
|
}, []string{"reason"})
|
|
)
|
|
|
|
type instance struct {
|
|
tenantID string
|
|
logger log.Logger
|
|
|
|
// Configuration
|
|
Cfg Config
|
|
|
|
// WAL and encoding
|
|
wal *wal.WAL
|
|
completeBlockEncoding encoding.VersionedEncoding
|
|
|
|
// Block management
|
|
blocksMtx sync.RWMutex
|
|
headBlock common.WALBlock
|
|
walBlocks map[uuid.UUID]common.WALBlock
|
|
completeBlocks map[uuid.UUID]*ingester.LocalBlock
|
|
lastCutTime time.Time
|
|
|
|
// Live traces
|
|
liveTracesMtx sync.Mutex
|
|
liveTraces *livetraces.LiveTraces[*v1.ResourceSpans]
|
|
traceSizes *tracesizes.Tracker
|
|
maxTraceLogger *util_log.RateLimitedLogger
|
|
|
|
// Metrics
|
|
tracesCreatedTotal prometheus.Counter
|
|
bytesReceivedTotal *prometheus.CounterVec
|
|
|
|
overrides overrides.Interface
|
|
}
|
|
|
|
func newInstance(instanceID string, cfg Config, wal *wal.WAL, completeBlockEncoding encoding.VersionedEncoding, overrides overrides.Interface, logger log.Logger) (*instance, error) {
|
|
logger = log.With(logger, "tenant", instanceID)
|
|
|
|
i := &instance{
|
|
tenantID: instanceID,
|
|
logger: logger,
|
|
Cfg: cfg,
|
|
wal: wal,
|
|
completeBlockEncoding: completeBlockEncoding,
|
|
walBlocks: map[uuid.UUID]common.WALBlock{},
|
|
completeBlocks: map[uuid.UUID]*ingester.LocalBlock{},
|
|
liveTraces: livetraces.New[*v1.ResourceSpans](func(rs *v1.ResourceSpans) uint64 { return uint64(rs.Size()) }, cfg.MaxTraceIdle, cfg.MaxTraceLive, instanceID),
|
|
traceSizes: tracesizes.New(),
|
|
maxTraceLogger: util_log.NewRateLimitedLogger(maxTraceLogLinesPerSecond, level.Warn(logger)),
|
|
overrides: overrides,
|
|
tracesCreatedTotal: metricTracesCreatedTotal.WithLabelValues(instanceID),
|
|
bytesReceivedTotal: metricBytesReceivedTotal,
|
|
// blockOffsetMeta: make(map[uuid.UUID]offsetMetadata),
|
|
}
|
|
|
|
err := i.resetHeadBlock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return i, nil
|
|
}
|
|
|
|
func (i *instance) backpressure(ctx context.Context) bool {
|
|
if i.Cfg.MaxLiveTracesBytes > 0 {
|
|
// Check live traces
|
|
i.liveTracesMtx.Lock()
|
|
sz := i.liveTraces.Size()
|
|
i.liveTracesMtx.Unlock()
|
|
|
|
if sz >= i.Cfg.MaxLiveTracesBytes {
|
|
// Live traces exceeds the expected amount of data in per wal flush,
|
|
// so wait a bit.
|
|
select {
|
|
case <-ctx.Done():
|
|
return false
|
|
case <-time.After(1 * time.Second):
|
|
}
|
|
|
|
metricBackPressure.WithLabelValues(reasonWaitingForLiveTraces).Inc()
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Check outstanding wal blocks
|
|
i.blocksMtx.RLock()
|
|
count := len(i.walBlocks)
|
|
i.blocksMtx.RUnlock()
|
|
|
|
if count > 1 {
|
|
// There are multiple outstanding WAL blocks that need completion
|
|
// so wait a bit.
|
|
select {
|
|
case <-ctx.Done():
|
|
return false
|
|
case <-time.After(1 * time.Second):
|
|
}
|
|
|
|
metricBackPressure.WithLabelValues(reasonWaitingForWAL).Inc()
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (i *instance) pushBytes(ctx context.Context, ts time.Time, req *tempopb.PushBytesRequest) {
|
|
if len(req.Traces) != len(req.Ids) {
|
|
level.Error(i.logger).Log("msg", "mismatched traces and ids length", "IDs", len(req.Ids), "traces", len(req.Traces))
|
|
return
|
|
}
|
|
|
|
// Wait for room in pipeline if needed
|
|
for i.backpressure(ctx) {
|
|
}
|
|
|
|
if err := ctx.Err(); err != nil {
|
|
level.Error(i.logger).Log("msg", "failed to push bytes to instance", "err", err)
|
|
return
|
|
}
|
|
|
|
// Check tenant limits
|
|
maxBytes := i.overrides.MaxBytesPerTrace(i.tenantID)
|
|
maxLiveTraces := i.overrides.MaxLocalTracesPerUser(i.tenantID)
|
|
|
|
// For each pre-marshalled trace, we need to unmarshal it and push to live traces
|
|
for j, traceBytes := range req.Traces {
|
|
traceID := req.Ids[j]
|
|
// measure received bytes as sum of slice lengths
|
|
// type byte is guaranteed to be 1 byte in size
|
|
// ref: https://golang.org/ref/spec#Size_and_alignment_guarantees
|
|
i.bytesReceivedTotal.WithLabelValues(i.tenantID, traceDataType).Add(float64(len(traceBytes.Slice)))
|
|
|
|
// Unmarshal the trace
|
|
trace := &tempopb.Trace{}
|
|
if err := trace.Unmarshal(traceBytes.Slice); err != nil {
|
|
level.Error(i.logger).Log("msg", "failed to unmarshal trace", "err", err)
|
|
continue
|
|
}
|
|
|
|
// Reuse the byte slice now that we've unmarshalled it
|
|
tempopb.ReuseByteSlices([][]byte{traceBytes.Slice})
|
|
|
|
// test max trace size. use trace sizes over liveTraces b/c it tracks large traces across multiple flushes
|
|
if maxBytes > 0 {
|
|
traceSz := trace.Size()
|
|
allowResult := i.traceSizes.Allow(traceID, traceSz, maxBytes)
|
|
if !allowResult.IsAllowed {
|
|
i.maxTraceLogger.Log("msg", overrides.ErrorPrefixTraceTooLarge, "max", maxBytes, "traceSz", traceSz, "totalSize", allowResult.CurrentTotalSize, "trace", hex.EncodeToString(traceID), "insight", true)
|
|
overrides.RecordDiscardedSpans(countSpans(trace), overrides.ReasonTraceTooLarge, i.tenantID)
|
|
continue
|
|
}
|
|
}
|
|
|
|
i.liveTracesMtx.Lock()
|
|
// Push each batch in the trace to live traces
|
|
for _, batch := range trace.ResourceSpans {
|
|
if len(batch.ScopeSpans) == 0 || len(batch.ScopeSpans[0].Spans) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Push to live traces with tenant-specific limits
|
|
if err := i.liveTraces.PushWithTimestampAndLimits(ts, traceID, batch, uint64(maxLiveTraces), 0); err != nil {
|
|
var reason string
|
|
switch {
|
|
case errors.Is(err, livetraces.ErrMaxLiveTracesExceeded):
|
|
reason = overrides.ReasonLiveTracesExceeded
|
|
case errors.Is(err, livetraces.ErrMaxTraceSizeExceeded): // this should technically never happen b/c we are passing 0 as max trace sz
|
|
reason = overrides.ReasonTraceTooLarge
|
|
default:
|
|
reason = overrides.ReasonUnknown
|
|
}
|
|
level.Debug(i.logger).Log("msg", "dropped spans due to limits", "tenant", i.tenantID, "reason", reason)
|
|
overrides.RecordDiscardedSpans(countSpans(trace), reason, i.tenantID)
|
|
continue
|
|
}
|
|
}
|
|
i.liveTracesMtx.Unlock()
|
|
}
|
|
}
|
|
|
|
func countSpans(trace *tempopb.Trace) int {
|
|
count := 0
|
|
for _, b := range trace.ResourceSpans {
|
|
for _, ss := range b.ScopeSpans {
|
|
count += len(ss.Spans)
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func (i *instance) cutIdleTraces(immediate bool) error {
|
|
i.liveTracesMtx.Lock()
|
|
// Set metrics before cutting (similar to ingester)
|
|
metricLiveTraces.WithLabelValues(i.tenantID).Set(float64(i.liveTraces.Len()))
|
|
metricLiveTraceBytes.WithLabelValues(i.tenantID).Set(float64(i.liveTraces.Size()))
|
|
|
|
tracesToCut := i.liveTraces.CutIdle(time.Now(), immediate)
|
|
i.liveTracesMtx.Unlock()
|
|
|
|
if len(tracesToCut) == 0 {
|
|
return nil
|
|
}
|
|
// Sort by ID
|
|
sort.Slice(tracesToCut, func(i, j int) bool {
|
|
return bytes.Compare(tracesToCut[i].ID, tracesToCut[j].ID) == -1
|
|
})
|
|
// Collect the trace IDs that will be flushed
|
|
for _, t := range tracesToCut {
|
|
err := i.writeHeadBlock(t.ID, t)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
i.tracesCreatedTotal.Inc()
|
|
}
|
|
|
|
i.blocksMtx.Lock()
|
|
defer i.blocksMtx.Unlock()
|
|
if i.headBlock != nil {
|
|
err := i.headBlock.Flush()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (i *instance) writeHeadBlock(id []byte, liveTrace *livetraces.LiveTrace[*v1.ResourceSpans]) error {
|
|
i.blocksMtx.Lock()
|
|
defer i.blocksMtx.Unlock()
|
|
|
|
if i.headBlock == nil {
|
|
err := i.resetHeadBlock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
tr := &tempopb.Trace{
|
|
ResourceSpans: liveTrace.Batches,
|
|
}
|
|
|
|
// Get trace timestamp bounds
|
|
var start, end uint64
|
|
for _, batch := range tr.ResourceSpans {
|
|
for _, ss := range batch.ScopeSpans {
|
|
for _, s := range ss.Spans {
|
|
if start == 0 || s.StartTimeUnixNano < start {
|
|
start = s.StartTimeUnixNano
|
|
}
|
|
if s.EndTimeUnixNano > end {
|
|
end = s.EndTimeUnixNano
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert from unix nanos to unix seconds
|
|
startSeconds := uint32(start / uint64(time.Second))
|
|
endSeconds := uint32(end / uint64(time.Second))
|
|
|
|
// constrain start/end with ingestion slack calculated off of liveTrace.createdAt and lastAppend
|
|
// createdAt and lastAppend are set via the record.Timestamp from kafka so they are "time.Now()" for the
|
|
// ingestion of this trace
|
|
slackDuration := i.Cfg.WAL.IngestionSlack
|
|
minStart := uint32(liveTrace.CreatedAt.Add(-slackDuration).Unix())
|
|
maxEnd := uint32(liveTrace.LastAppend.Add(slackDuration).Unix())
|
|
|
|
if startSeconds < minStart {
|
|
startSeconds = minStart
|
|
}
|
|
if endSeconds > maxEnd {
|
|
endSeconds = maxEnd
|
|
}
|
|
|
|
return i.headBlock.AppendTrace(id, tr, startSeconds, endSeconds, false)
|
|
}
|
|
|
|
func (i *instance) resetHeadBlock() error {
|
|
dedicatedColumns := i.overrides.DedicatedColumns(i.tenantID)
|
|
|
|
meta := &backend.BlockMeta{
|
|
BlockID: backend.NewUUID(),
|
|
TenantID: i.tenantID,
|
|
DedicatedColumns: dedicatedColumns,
|
|
ReplicationFactor: backend.LiveStoreReplicationFactor,
|
|
}
|
|
block, err := i.wal.NewBlock(meta, model.CurrentEncoding)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
i.headBlock = block
|
|
i.lastCutTime = time.Now()
|
|
return nil
|
|
}
|
|
|
|
func (i *instance) cutBlocks(immediate bool) (uuid.UUID, error) {
|
|
i.blocksMtx.Lock()
|
|
defer i.blocksMtx.Unlock()
|
|
|
|
if i.headBlock == nil || i.headBlock.DataLength() == 0 {
|
|
return uuid.Nil, nil
|
|
}
|
|
|
|
if !immediate && time.Since(i.lastCutTime) < i.Cfg.MaxBlockDuration && i.headBlock.DataLength() < i.Cfg.MaxBlockBytes {
|
|
return uuid.Nil, nil
|
|
}
|
|
|
|
i.traceSizes.ClearIdle(i.lastCutTime)
|
|
|
|
// Final flush
|
|
err := i.headBlock.Flush()
|
|
if err != nil {
|
|
return uuid.Nil, err
|
|
}
|
|
|
|
id := (uuid.UUID)(i.headBlock.BlockMeta().BlockID)
|
|
blockSize := i.headBlock.DataLength()
|
|
i.walBlocks[id] = i.headBlock
|
|
|
|
level.Info(i.logger).Log("msg", "queueing wal block for completion", "block", id.String(), "size", blockSize)
|
|
|
|
err = i.resetHeadBlock()
|
|
if err != nil {
|
|
return uuid.Nil, err
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
func (i *instance) completeBlock(ctx context.Context, id uuid.UUID) error {
|
|
ctx, span := tracer.Start(ctx, "instance.completeBlock",
|
|
oteltrace.WithAttributes(
|
|
attribute.String("tenant", i.tenantID),
|
|
attribute.String("blockID", id.String()),
|
|
))
|
|
defer span.End()
|
|
|
|
i.blocksMtx.Lock()
|
|
walBlock := i.walBlocks[id]
|
|
i.blocksMtx.Unlock()
|
|
|
|
if walBlock == nil {
|
|
level.Warn(i.logger).Log("msg", "WAL block disappeared before being completed", "id", id)
|
|
span.AddEvent("WAL block not found")
|
|
return nil
|
|
}
|
|
|
|
blockSize := walBlock.DataLength()
|
|
metricCompletionSize.Observe(float64(blockSize))
|
|
span.SetAttributes(attribute.Int64("block_size", int64(blockSize)))
|
|
|
|
// Create completed block
|
|
reader := backend.NewReader(i.wal.LocalBackend())
|
|
writer := backend.NewWriter(i.wal.LocalBackend())
|
|
|
|
iter, err := walBlock.Iterator()
|
|
if err != nil {
|
|
level.Error(i.logger).Log("msg", "failed to get WAL block iterator", "id", id, "err", err)
|
|
span.RecordError(err)
|
|
return err
|
|
}
|
|
defer iter.Close()
|
|
|
|
newMeta, err := i.completeBlockEncoding.CreateBlock(ctx, &i.Cfg.BlockConfig, walBlock.BlockMeta(), iter, reader, writer)
|
|
if err != nil {
|
|
level.Error(i.logger).Log("msg", "failed to create complete block", "id", id, "err", err)
|
|
span.RecordError(err)
|
|
return err
|
|
}
|
|
|
|
newBlock, err := i.completeBlockEncoding.OpenBlock(newMeta, reader)
|
|
if err != nil {
|
|
level.Error(i.logger).Log("msg", "failed to open complete block", "id", id, "err", err)
|
|
span.RecordError(err)
|
|
return err
|
|
}
|
|
|
|
i.blocksMtx.Lock()
|
|
defer i.blocksMtx.Unlock()
|
|
|
|
// Verify the WAL block still exists
|
|
if _, ok := i.walBlocks[id]; !ok {
|
|
level.Warn(i.logger).Log("msg", "WAL block disappeared while being completed, deleting complete block", "id", id)
|
|
err := i.wal.LocalBackend().ClearBlock(id, i.tenantID)
|
|
if err != nil {
|
|
level.Error(i.logger).Log("msg", "failed to clear complete block after WAL disappeared", "block", id, "err", err)
|
|
}
|
|
span.AddEvent("WAL block disappeared during completion")
|
|
return nil
|
|
}
|
|
|
|
i.completeBlocks[id] = ingester.NewLocalBlock(ctx, newBlock, i.wal.LocalBackend())
|
|
|
|
err = walBlock.Clear()
|
|
if err != nil {
|
|
level.Error(i.logger).Log("msg", "failed to clear WAL block", "id", id, "err", err)
|
|
span.RecordError(err)
|
|
}
|
|
delete(i.walBlocks, (uuid.UUID)(walBlock.BlockMeta().BlockID))
|
|
|
|
level.Info(i.logger).Log("msg", "completed block", "id", id.String())
|
|
span.AddEvent("block completed successfully")
|
|
return nil
|
|
}
|
|
|
|
func (i *instance) deleteOldBlocks() error {
|
|
i.blocksMtx.Lock()
|
|
defer i.blocksMtx.Unlock()
|
|
|
|
cutoff := time.Now().Add(-i.Cfg.CompleteBlockTimeout) // Delete blocks older than Complete Block Timeout
|
|
|
|
for id, walBlock := range i.walBlocks {
|
|
if walBlock.BlockMeta().EndTime.Before(cutoff) {
|
|
if _, ok := i.completeBlocks[id]; !ok {
|
|
level.Warn(i.logger).Log("msg", "deleting WAL block that was never completed", "block", id.String())
|
|
}
|
|
err := walBlock.Clear()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
delete(i.walBlocks, id)
|
|
metricBlocksClearedTotal.WithLabelValues("wal").Inc()
|
|
}
|
|
}
|
|
|
|
for id, completeBlock := range i.completeBlocks {
|
|
if completeBlock.BlockMeta().EndTime.Before(cutoff) {
|
|
|
|
level.Info(i.logger).Log("msg", "deleting complete block", "block", id.String())
|
|
err := i.wal.LocalBackend().ClearBlock(id, i.tenantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
delete(i.completeBlocks, id)
|
|
metricBlocksClearedTotal.WithLabelValues("complete").Inc()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|