mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
1029 lines
26 KiB
Go
1029 lines
26 KiB
Go
package localblocks
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
kitlog "github.com/go-kit/log"
|
|
"github.com/go-kit/log/level"
|
|
"github.com/golang/groupcache/lru"
|
|
"github.com/google/uuid"
|
|
"github.com/grafana/tempo/modules/ingester"
|
|
"github.com/grafana/tempo/pkg/flushqueues"
|
|
"github.com/grafana/tempo/pkg/tracesizes"
|
|
"github.com/grafana/tempo/tempodb"
|
|
"go.opentelemetry.io/otel"
|
|
|
|
gen "github.com/grafana/tempo/modules/generator/processor"
|
|
"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/traceql"
|
|
"github.com/grafana/tempo/pkg/traceqlmetrics"
|
|
"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"
|
|
)
|
|
|
|
var tracer = otel.Tracer("modules/generator/processor/localblocks")
|
|
|
|
const (
|
|
timeBuffer = 5 * time.Minute
|
|
maxFlushAttempts = 100
|
|
)
|
|
|
|
// ProcessorOverrides is just the set of overrides needed here.
|
|
type ProcessorOverrides interface {
|
|
DedicatedColumns(string) backend.DedicatedColumns
|
|
MaxLocalTracesPerUser(userID string) int
|
|
MaxBytesPerTrace(string) int
|
|
UnsafeQueryHints(string) bool
|
|
}
|
|
|
|
type Processor struct {
|
|
tenant string
|
|
logger kitlog.Logger
|
|
Cfg Config
|
|
wal *wal.WAL
|
|
walR backend.Reader
|
|
walW backend.Writer
|
|
ctx context.Context
|
|
cancel func()
|
|
wg sync.WaitGroup
|
|
cacheMtx sync.RWMutex
|
|
cache *lru.Cache
|
|
overrides ProcessorOverrides
|
|
enc encoding.VersionedEncoding
|
|
|
|
blocksMtx sync.RWMutex
|
|
headBlock common.WALBlock
|
|
walBlocks map[uuid.UUID]common.WALBlock
|
|
completeBlocks map[uuid.UUID]*ingester.LocalBlock
|
|
lastCutTime time.Time
|
|
|
|
flushqueue *flushqueues.PriorityQueue[*flushOp]
|
|
|
|
liveTracesMtx sync.Mutex
|
|
liveTraces *livetraces.LiveTraces[*v1.ResourceSpans]
|
|
traceSizes *tracesizes.Tracker
|
|
|
|
writer tempodb.Writer
|
|
}
|
|
|
|
var _ gen.Processor = (*Processor)(nil)
|
|
|
|
func New(cfg Config, tenant string, wal *wal.WAL, writer tempodb.Writer, overrides ProcessorOverrides) (p *Processor, err error) {
|
|
if wal == nil {
|
|
return nil, errors.New("local blocks processor requires traces wal")
|
|
}
|
|
|
|
enc := encoding.DefaultEncoding()
|
|
if cfg.Block.Version != "" {
|
|
enc, err = encoding.FromVersionForWrites(cfg.Block.Version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
p = &Processor{
|
|
tenant: tenant,
|
|
logger: log.WithUserID(tenant, log.Logger),
|
|
Cfg: cfg,
|
|
wal: wal,
|
|
walR: backend.NewReader(wal.LocalBackend()),
|
|
walW: backend.NewWriter(wal.LocalBackend()),
|
|
overrides: overrides,
|
|
enc: enc,
|
|
walBlocks: map[uuid.UUID]common.WALBlock{},
|
|
completeBlocks: map[uuid.UUID]*ingester.LocalBlock{},
|
|
flushqueue: flushqueues.NewPriorityQueue[*flushOp](metricFlushQueueSize.WithLabelValues(tenant)),
|
|
liveTraces: livetraces.New[*v1.ResourceSpans](func(rs *v1.ResourceSpans) uint64 { return uint64(rs.Size()) }, cfg.TraceIdlePeriod, cfg.TraceLivePeriod, tenant),
|
|
traceSizes: tracesizes.New(),
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
wg: sync.WaitGroup{},
|
|
cache: lru.New(100),
|
|
writer: writer,
|
|
}
|
|
|
|
startCompleteQueue(p.Cfg.Concurrency)
|
|
defer func() {
|
|
if err != nil {
|
|
// In case of failing startup stop the queue
|
|
// because Shutdown will not be called.
|
|
stopCompleteQueue()
|
|
}
|
|
}()
|
|
|
|
err = p.reloadBlocks()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("replaying blocks: %w", err)
|
|
}
|
|
|
|
p.wg.Add(3)
|
|
go p.cutLoop()
|
|
go p.deleteLoop()
|
|
go p.metricLoop()
|
|
|
|
if p.writer != nil && p.Cfg.FlushToStorage {
|
|
p.wg.Add(1)
|
|
go p.flushLoop()
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
func (*Processor) Name() string {
|
|
return gen.LocalBlocksName
|
|
}
|
|
|
|
func (p *Processor) PushSpans(_ context.Context, req *tempopb.PushSpansRequest) {
|
|
p.push(time.Now(), req)
|
|
}
|
|
|
|
func (p *Processor) DeterministicPush(ts time.Time, req *tempopb.PushSpansRequest) {
|
|
if time.Since(ts) > p.Cfg.CompleteBlockTimeout {
|
|
// Ignore data that is beyond retention
|
|
return
|
|
}
|
|
|
|
// Wait for room in pipeline if needed
|
|
for p.backpressure() {
|
|
}
|
|
|
|
p.push(ts, req)
|
|
}
|
|
|
|
func (p *Processor) backpressure() bool {
|
|
if p.Cfg.MaxLiveTracesBytes > 0 {
|
|
// Check live traces
|
|
p.liveTracesMtx.Lock()
|
|
liveTracesSize := p.liveTraces.Size()
|
|
p.liveTracesMtx.Unlock()
|
|
|
|
if liveTracesSize >= p.Cfg.MaxLiveTracesBytes {
|
|
// Live traces exceeds the expected amount of data in
|
|
// per wal flush, so wait a bit.
|
|
select {
|
|
case <-p.ctx.Done():
|
|
case <-time.After(1 * time.Second):
|
|
}
|
|
|
|
metricBackPressure.WithLabelValues(reasonWaitingForLiveTraces).Inc()
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Check outstanding wal blocks
|
|
p.blocksMtx.RLock()
|
|
count := len(p.walBlocks)
|
|
p.blocksMtx.RUnlock()
|
|
|
|
if count > 1 {
|
|
// There are multiple outstanding WAL blocks that need completion
|
|
// so wait a bit.
|
|
select {
|
|
case <-p.ctx.Done():
|
|
case <-time.After(1 * time.Second):
|
|
}
|
|
|
|
metricBackPressure.WithLabelValues(reasonWaitingForWAL).Inc()
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (p *Processor) push(ts time.Time, req *tempopb.PushSpansRequest) {
|
|
p.liveTracesMtx.Lock()
|
|
defer p.liveTracesMtx.Unlock()
|
|
|
|
var (
|
|
before = p.liveTraces.Len()
|
|
maxLen = p.maxLiveTraces()
|
|
maxSz = p.overrides.MaxBytesPerTrace(p.tenant)
|
|
batches = req.Batches
|
|
)
|
|
|
|
if p.Cfg.FilterServerSpans {
|
|
batches = filterBatches(batches)
|
|
}
|
|
|
|
for _, batch := range batches {
|
|
|
|
// Spans in the batch are for the same trace.
|
|
// We use the first one.
|
|
if len(batch.ScopeSpans) == 0 || len(batch.ScopeSpans[0].Spans) == 0 {
|
|
continue
|
|
}
|
|
traceID := batch.ScopeSpans[0].Spans[0].TraceId
|
|
|
|
// Metric total spans regardless of outcome
|
|
numSpans := 0
|
|
for _, ss := range batch.ScopeSpans {
|
|
numSpans += len(ss.Spans)
|
|
}
|
|
metricTotalSpans.WithLabelValues(p.tenant).Add(float64(numSpans))
|
|
|
|
// Check max trace size
|
|
if maxSz > 0 {
|
|
allowResult := p.traceSizes.Allow(traceID, batch.Size(), maxSz)
|
|
if !allowResult.IsAllowed {
|
|
metricDroppedSpans.WithLabelValues(p.tenant, reasonTraceSizeExceeded).Add(float64(numSpans))
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Live traces
|
|
// Doesn't assert trace size because that is done above using traceSizes
|
|
// which tracks it across flushes.
|
|
if err := p.liveTraces.PushWithTimestampAndLimits(ts, traceID, batch, maxLen, 0); err != nil {
|
|
metricDroppedTraces.WithLabelValues(p.tenant, reasonLiveTracesExceeded).Inc()
|
|
continue
|
|
}
|
|
}
|
|
|
|
after := p.liveTraces.Len()
|
|
|
|
// Number of new traces is the delta
|
|
metricTotalTraces.WithLabelValues(p.tenant).Add(float64(after - before))
|
|
}
|
|
|
|
// maxLiveTraces for the tenant, if enabled, and read from config in order of precedence.
|
|
func (p *Processor) maxLiveTraces() uint64 {
|
|
if m := p.overrides.MaxLocalTracesPerUser(p.tenant); m > 0 {
|
|
return uint64(m)
|
|
}
|
|
|
|
return p.Cfg.MaxLiveTraces
|
|
}
|
|
|
|
func (p *Processor) Shutdown(context.Context) {
|
|
p.cancel()
|
|
p.wg.Wait()
|
|
|
|
// Immediately cut all traces from memory
|
|
err := p.cutIdleTraces(true)
|
|
if err != nil {
|
|
level.Error(p.logger).Log("msg", "local blocks processor failed to cut remaining traces on shutdown", "err", err)
|
|
}
|
|
|
|
err = p.cutBlocks(true)
|
|
if err != nil {
|
|
level.Error(p.logger).Log("msg", "local blocks processor failed to cut head block on shutdown", "err", err)
|
|
}
|
|
|
|
stopCompleteQueue()
|
|
}
|
|
|
|
func (p *Processor) cutLoop() {
|
|
defer p.wg.Done()
|
|
|
|
flushTicker := time.NewTicker(p.Cfg.FlushCheckPeriod)
|
|
defer flushTicker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-flushTicker.C:
|
|
err := p.cutIdleTraces(false)
|
|
if err != nil {
|
|
level.Error(p.logger).Log("msg", "local blocks processor failed to cut idle traces", "err", err)
|
|
}
|
|
|
|
err = p.cutBlocks(false)
|
|
if err != nil {
|
|
level.Error(p.logger).Log("msg", "local blocks processor failed to cut head block", "err", err)
|
|
}
|
|
|
|
case <-p.ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Processor) deleteLoop() {
|
|
defer p.wg.Done()
|
|
|
|
ticker := time.NewTicker(10 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
err := p.deleteOldBlocks()
|
|
if err != nil {
|
|
level.Error(p.logger).Log("msg", "local blocks processor failed to delete old blocks", "err", err)
|
|
}
|
|
|
|
case <-p.ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Processor) flushLoop() {
|
|
defer p.wg.Done()
|
|
|
|
go func() {
|
|
<-p.ctx.Done()
|
|
p.flushqueue.Close()
|
|
}()
|
|
|
|
for {
|
|
op := p.flushqueue.Dequeue()
|
|
if op == nil {
|
|
return
|
|
}
|
|
|
|
op.attempts++
|
|
|
|
if op.attempts > maxFlushAttempts {
|
|
_ = level.Error(p.logger).Log("msg", "failed to flush block after max attempts", "tenant", p.tenant, "block", op.blockID, "attempts", op.attempts)
|
|
|
|
// attempt to delete the block
|
|
p.blocksMtx.Lock()
|
|
err := p.wal.LocalBackend().ClearBlock(op.blockID, p.tenant)
|
|
if err != nil {
|
|
_ = level.Error(p.logger).Log("msg", "failed to clear corrupt block", "tenant", p.tenant, "block", op.blockID, "err", err)
|
|
}
|
|
delete(p.completeBlocks, op.blockID)
|
|
p.blocksMtx.Unlock()
|
|
|
|
continue
|
|
}
|
|
|
|
err := p.flushBlock(op.blockID)
|
|
if err != nil {
|
|
_ = level.Info(p.logger).Log("msg", "re-queueing block for flushing", "block", op.blockID, "attempts", op.attempts, "err", err)
|
|
metricFailedFlushes.Inc()
|
|
|
|
delay := op.backoff()
|
|
op.at = time.Now().Add(delay)
|
|
|
|
go func() {
|
|
time.Sleep(delay)
|
|
if _, err := p.flushqueue.Enqueue(op); err != nil {
|
|
_ = level.Error(p.logger).Log("msg", "failed to requeue block for flushing", "err", err)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Processor) metricLoop() {
|
|
defer p.wg.Done()
|
|
|
|
ticker := time.NewTicker(10 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
// Instead of reacting to every block flush/update, just run on a timer.
|
|
p.recordBlockBytes()
|
|
|
|
case <-p.ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Processor) completeBlock(id uuid.UUID) error {
|
|
p.blocksMtx.RLock()
|
|
b := p.walBlocks[id]
|
|
p.blocksMtx.RUnlock()
|
|
|
|
if b == nil {
|
|
_ = level.Warn(p.logger).Log("msg", "local blocks processor WAL block disappeared before being completed", "id", id)
|
|
return nil
|
|
}
|
|
|
|
// Now create a new block
|
|
var (
|
|
ctx = p.ctx
|
|
reader = backend.NewReader(p.wal.LocalBackend())
|
|
writer = backend.NewWriter(p.wal.LocalBackend())
|
|
cfg = p.Cfg.Block
|
|
)
|
|
|
|
ctx, span := tracer.Start(ctx, "Processor.CompleteBlock")
|
|
defer span.End()
|
|
|
|
iter, err := b.Iterator()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer iter.Close()
|
|
|
|
newMeta, err := p.enc.CreateBlock(ctx, cfg, b.BlockMeta(), iter, reader, writer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newBlock, err := p.enc.OpenBlock(newMeta, reader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add new block and delete old block
|
|
p.blocksMtx.Lock()
|
|
defer p.blocksMtx.Unlock()
|
|
|
|
// Verify the WAL block still exists, it's possible that it went out of retention
|
|
// while it was being completed.
|
|
if _, ok := p.walBlocks[id]; !ok {
|
|
// WAL block is gone.
|
|
_ = level.Warn(p.logger).Log("msg", "local blocks processor WAL block disappeared while being completed, deleting complete block", "id", id)
|
|
err := p.wal.LocalBackend().ClearBlock(id, p.tenant)
|
|
if err != nil {
|
|
_ = level.Error(p.logger).Log("msg", "failed to clear complete block after WAL disappeared", "tenant", p.tenant, "block", id, "err", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
p.completeBlocks[id] = ingester.NewLocalBlock(ctx, newBlock, p.wal.LocalBackend())
|
|
metricCompletedBlocks.WithLabelValues(p.tenant).Inc()
|
|
|
|
// Queue for flushing
|
|
if p.Cfg.FlushToStorage {
|
|
if _, err := p.flushqueue.Enqueue(newFlushOp((uuid.UUID)(newMeta.BlockID))); err != nil {
|
|
_ = level.Error(p.logger).Log("msg", "local blocks processor failed to enqueue block for flushing", "err", err)
|
|
}
|
|
}
|
|
|
|
err = b.Clear()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
delete(p.walBlocks, (uuid.UUID)(b.BlockMeta().BlockID))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Processor) flushBlock(id uuid.UUID) error {
|
|
p.blocksMtx.RLock()
|
|
completeBlock := p.completeBlocks[id]
|
|
p.blocksMtx.RUnlock()
|
|
|
|
if completeBlock == nil {
|
|
return nil
|
|
}
|
|
|
|
err := p.writer.WriteBlock(p.ctx, completeBlock)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
metricFlushedBlocks.WithLabelValues(p.tenant).Inc()
|
|
_ = level.Info(p.logger).Log("msg", "flushed block to storage", "block", id.String())
|
|
return nil
|
|
}
|
|
|
|
func (p *Processor) GetMetrics(ctx context.Context, req *tempopb.SpanMetricsRequest) (*tempopb.SpanMetricsResponse, error) {
|
|
p.blocksMtx.RLock()
|
|
defer p.blocksMtx.RUnlock()
|
|
|
|
var (
|
|
err error
|
|
startNano = uint64(time.Unix(int64(req.Start), 0).UnixNano())
|
|
endNano = uint64(time.Unix(int64(req.End), 0).UnixNano())
|
|
)
|
|
|
|
if startNano > 0 && endNano > 0 {
|
|
cutoff := time.Now().Add(-p.Cfg.CompleteBlockTimeout).Add(-timeBuffer)
|
|
if startNano < uint64(cutoff.UnixNano()) {
|
|
return nil, fmt.Errorf("time range must be within last %v", p.Cfg.CompleteBlockTimeout)
|
|
}
|
|
}
|
|
|
|
// Blocks to check
|
|
blocks := make([]common.BackendBlock, 0, 1+len(p.walBlocks)+len(p.completeBlocks))
|
|
if p.headBlock != nil {
|
|
blocks = append(blocks, p.headBlock)
|
|
}
|
|
for _, b := range p.walBlocks {
|
|
blocks = append(blocks, b)
|
|
}
|
|
for _, b := range p.completeBlocks {
|
|
blocks = append(blocks, b)
|
|
}
|
|
|
|
m := traceqlmetrics.NewMetricsResults()
|
|
for _, b := range blocks {
|
|
|
|
var (
|
|
meta = b.BlockMeta()
|
|
blockStart = uint32(meta.StartTime.Unix())
|
|
blockEnd = uint32(meta.EndTime.Unix())
|
|
// We can only cache the results of this query on this block
|
|
// if the time range fully covers this block (not partial).
|
|
cacheable = req.Start <= blockStart && req.End >= blockEnd
|
|
// Including the trace count in the cache key means we can safely
|
|
// cache results for a wal block which can receive new data
|
|
key = fmt.Sprintf("b:%s-c:%d-q:%s-g:%s", meta.BlockID.String(), meta.TotalObjects, req.Query, req.GroupBy)
|
|
)
|
|
|
|
var r *traceqlmetrics.MetricsResults
|
|
|
|
if cacheable {
|
|
r = p.metricsCacheGet(key)
|
|
}
|
|
|
|
// Uncacheable or not found in cache
|
|
if r == nil {
|
|
|
|
f := traceql.NewSpansetFetcherWrapper(func(ctx context.Context, req traceql.FetchSpansRequest) (traceql.FetchSpansResponse, error) {
|
|
return b.Fetch(ctx, req, common.DefaultSearchOptions())
|
|
})
|
|
|
|
r, err = traceqlmetrics.GetMetrics(ctx, req.Query, req.GroupBy, 0, startNano, endNano, f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if cacheable {
|
|
p.metricsCacheSet(key, r)
|
|
}
|
|
}
|
|
|
|
m.Combine(r)
|
|
|
|
if req.Limit > 0 && m.SpanCount >= int(req.Limit) {
|
|
break
|
|
}
|
|
}
|
|
|
|
resp := &tempopb.SpanMetricsResponse{
|
|
SpanCount: uint64(m.SpanCount),
|
|
Estimated: m.Estimated,
|
|
Metrics: make([]*tempopb.SpanMetrics, 0, len(m.Series)),
|
|
}
|
|
|
|
var rawHistorgram *tempopb.RawHistogram
|
|
var errCount int
|
|
for keys, sh := range m.Series {
|
|
h := []*tempopb.RawHistogram{}
|
|
|
|
for bucket, count := range sh.Histogram.Buckets() {
|
|
if count != 0 {
|
|
rawHistorgram = &tempopb.RawHistogram{
|
|
Bucket: uint64(bucket),
|
|
Count: uint64(count),
|
|
}
|
|
|
|
h = append(h, rawHistorgram)
|
|
}
|
|
}
|
|
|
|
errCount = 0
|
|
if errs, ok := m.Errors[keys]; ok {
|
|
errCount = errs
|
|
}
|
|
|
|
resp.Metrics = append(resp.Metrics, &tempopb.SpanMetrics{
|
|
LatencyHistogram: h,
|
|
Series: metricSeriesToProto(sh.Series),
|
|
Errors: uint64(errCount),
|
|
})
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func (p *Processor) metricsCacheGet(key string) *traceqlmetrics.MetricsResults {
|
|
p.cacheMtx.RLock()
|
|
defer p.cacheMtx.RUnlock()
|
|
|
|
if r, ok := p.cache.Get(key); ok {
|
|
return r.(*traceqlmetrics.MetricsResults)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Processor) metricsCacheSet(key string, m *traceqlmetrics.MetricsResults) {
|
|
p.cacheMtx.Lock()
|
|
defer p.cacheMtx.Unlock()
|
|
|
|
p.cache.Add(key, m)
|
|
}
|
|
|
|
func (p *Processor) deleteOldBlocks() (err error) {
|
|
p.blocksMtx.Lock()
|
|
defer p.blocksMtx.Unlock()
|
|
|
|
cuttoff := time.Now().Add(-p.Cfg.CompleteBlockTimeout)
|
|
|
|
for id, b := range p.walBlocks {
|
|
if b.BlockMeta().EndTime.Before(cuttoff) {
|
|
if _, ok := p.completeBlocks[id]; !ok {
|
|
level.Warn(p.logger).Log("msg", "deleting WAL block that was never completed", "block", id.String())
|
|
}
|
|
err = b.Clear()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
delete(p.walBlocks, id)
|
|
}
|
|
}
|
|
|
|
for id, b := range p.completeBlocks {
|
|
if !p.Cfg.FlushToStorage {
|
|
if b.BlockMeta().EndTime.Before(cuttoff) {
|
|
level.Info(p.logger).Log("msg", "deleting complete block", "block", id.String())
|
|
err = p.wal.LocalBackend().ClearBlock(id, p.tenant)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
delete(p.completeBlocks, id)
|
|
}
|
|
continue
|
|
}
|
|
|
|
flushedTime := b.FlushedTime()
|
|
if flushedTime.IsZero() {
|
|
continue
|
|
}
|
|
|
|
if b.BlockMeta().EndTime.Before(cuttoff) {
|
|
level.Info(p.logger).Log("msg", "deleting flushed complete block", "block", id.String())
|
|
err = p.wal.LocalBackend().ClearBlock(id, p.tenant)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
delete(p.completeBlocks, id)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (p *Processor) cutIdleTraces(immediate bool) error {
|
|
p.liveTracesMtx.Lock()
|
|
|
|
// Record live traces before flushing so we know the high water mark
|
|
metricLiveTraces.WithLabelValues(p.tenant).Set(float64(p.liveTraces.Len()))
|
|
metricLiveTraceBytes.WithLabelValues(p.tenant).Set(float64(p.liveTraces.Size()))
|
|
|
|
tracesToCut := p.liveTraces.CutIdle(time.Now(), immediate)
|
|
|
|
p.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
|
|
})
|
|
|
|
for _, t := range tracesToCut {
|
|
|
|
tr := &tempopb.Trace{
|
|
ResourceSpans: t.Batches,
|
|
}
|
|
|
|
err := p.writeHeadBlock(t.ID, tr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
}
|
|
|
|
p.blocksMtx.Lock()
|
|
defer p.blocksMtx.Unlock()
|
|
return p.headBlock.Flush()
|
|
}
|
|
|
|
func (p *Processor) writeHeadBlock(id common.ID, tr *tempopb.Trace) error {
|
|
p.blocksMtx.Lock()
|
|
defer p.blocksMtx.Unlock()
|
|
|
|
if p.headBlock == nil {
|
|
err := p.resetHeadBlock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Get trace timestamp bounds
|
|
var start, end uint64
|
|
for _, b := range tr.ResourceSpans {
|
|
for _, ss := range b.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))
|
|
|
|
err := p.headBlock.AppendTrace(id, tr, startSeconds, endSeconds, p.Cfg.AdjustTimeRangeForSlack)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Processor) resetHeadBlock() error {
|
|
meta := &backend.BlockMeta{
|
|
BlockID: backend.NewUUID(),
|
|
TenantID: p.tenant,
|
|
DedicatedColumns: p.overrides.DedicatedColumns(p.tenant),
|
|
ReplicationFactor: backend.MetricsGeneratorReplicationFactor,
|
|
}
|
|
block, err := p.wal.NewBlock(meta, model.CurrentEncoding)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.headBlock = block
|
|
p.lastCutTime = time.Now()
|
|
return nil
|
|
}
|
|
|
|
func (p *Processor) cutBlocks(immediate bool) error {
|
|
p.blocksMtx.Lock()
|
|
defer p.blocksMtx.Unlock()
|
|
|
|
if p.headBlock == nil || p.headBlock.DataLength() == 0 {
|
|
return nil
|
|
}
|
|
|
|
if !immediate && time.Since(p.lastCutTime) < p.Cfg.MaxBlockDuration && p.headBlock.DataLength() < p.Cfg.MaxBlockBytes {
|
|
return nil
|
|
}
|
|
|
|
// Clear historical trace sizes for traces that weren't seen in this block.
|
|
p.traceSizes.ClearIdle(p.lastCutTime)
|
|
|
|
// Final flush
|
|
err := p.headBlock.Flush()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to flush head block: %w", err)
|
|
}
|
|
|
|
id := (uuid.UUID)(p.headBlock.BlockMeta().BlockID)
|
|
p.walBlocks[id] = p.headBlock
|
|
metricCutBlocks.WithLabelValues(p.tenant).Inc()
|
|
|
|
err = p.resetHeadBlock()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to resetHeadBlock: %w", err)
|
|
}
|
|
|
|
level.Info(p.logger).Log("msg", "queueing wal block for completion", "block", id.String())
|
|
if err := enqueueCompleteOp(p.ctx, p, id); err != nil {
|
|
_ = level.Error(p.logger).Log("msg", "local blocks processor failed to enqueue block for completion", "err", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Processor) reloadBlocks() error {
|
|
var (
|
|
ctx = p.ctx
|
|
t = p.tenant
|
|
l = p.wal.LocalBackend()
|
|
r = backend.NewReader(l)
|
|
)
|
|
|
|
// ------------------------------------
|
|
// wal blocks
|
|
// ------------------------------------
|
|
level.Info(p.logger).Log("msg", "reloading wal blocks")
|
|
walBlocks, err := p.wal.RescanBlocks(0, log.Logger)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Important - Must take the lock while adding to the block lists and enqueuing work,
|
|
// but don't take it until after the slowest RescanBlocks step above to prevent blocking
|
|
// other work for other tenants. All of the below steps are fairly quick.
|
|
p.blocksMtx.Lock()
|
|
defer p.blocksMtx.Unlock()
|
|
|
|
for _, blk := range walBlocks {
|
|
meta := blk.BlockMeta()
|
|
if meta.TenantID == p.tenant {
|
|
level.Info(p.logger).Log("msg", "reloading wal block", "block", meta.BlockID.String())
|
|
p.walBlocks[(uuid.UUID)(meta.BlockID)] = blk
|
|
|
|
level.Info(p.logger).Log("msg", "queueing replayed wal block for completion", "block", meta.BlockID.String())
|
|
if err := enqueueCompleteOp(p.ctx, p, uuid.UUID(meta.BlockID)); err != nil {
|
|
_ = level.Error(p.logger).Log("msg", "local blocks processor failed to enqueue wal block for completion after replay", "err", err)
|
|
}
|
|
}
|
|
}
|
|
level.Info(p.logger).Log("msg", "reloaded wal blocks", "count", len(p.walBlocks))
|
|
|
|
// ------------------------------------
|
|
// Complete blocks
|
|
// ------------------------------------
|
|
|
|
// This is a quirk of the local backend, we shouldn't try to list
|
|
// blocks until after we've made sure the tenant folder exists.
|
|
tenants, err := r.Tenants(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(tenants) == 0 {
|
|
level.Info(p.logger).Log("msg", "no tenants found, skipping complete block replay")
|
|
return nil
|
|
}
|
|
|
|
ids, _, err := r.Blocks(ctx, p.tenant)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
level.Info(p.logger).Log("msg", "reloading complete blocks", "count", len(ids))
|
|
|
|
for _, id := range ids {
|
|
level.Info(p.logger).Log("msg", "reloading complete block", "block", id.String())
|
|
meta, err := r.BlockMeta(ctx, id, t)
|
|
|
|
var clearBlock bool
|
|
if err != nil {
|
|
var vv *json.SyntaxError
|
|
if errors.Is(err, backend.ErrDoesNotExist) || errors.As(err, &vv) {
|
|
clearBlock = true
|
|
}
|
|
}
|
|
|
|
if clearBlock {
|
|
level.Info(p.logger).Log("msg", "clearing block", "block", id.String(), "err", err)
|
|
// Partially written block, delete and continue
|
|
err = l.ClearBlock(id, t)
|
|
if err != nil {
|
|
level.Error(p.logger).Log("msg", "local blocks processor failed to clear partially written block during replay", "err", err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
blk, err := encoding.OpenBlock(meta, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
level.Info(p.logger).Log("msg", "reloaded complete block", "block", id.String())
|
|
|
|
lb := ingester.NewLocalBlock(ctx, blk, l)
|
|
p.completeBlocks[id] = lb
|
|
|
|
if p.Cfg.FlushToStorage && lb.FlushedTime().IsZero() {
|
|
level.Info(p.logger).Log("msg", "queueing reloaded block for flushing", "block", id.String())
|
|
if _, err := p.flushqueue.Enqueue(newFlushOp(id)); err != nil {
|
|
_ = level.Error(p.logger).Log("msg", "local blocks processor failed to enqueue block for flushing during replay", "err", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Processor) recordBlockBytes() {
|
|
p.blocksMtx.RLock()
|
|
defer p.blocksMtx.RUnlock()
|
|
|
|
sum := uint64(0)
|
|
|
|
if p.headBlock != nil {
|
|
sum += p.headBlock.DataLength()
|
|
}
|
|
for _, b := range p.walBlocks {
|
|
sum += b.DataLength()
|
|
}
|
|
for _, b := range p.completeBlocks {
|
|
sum += b.BlockMeta().Size_
|
|
}
|
|
|
|
metricBlockSize.WithLabelValues(p.tenant).Set(float64(sum))
|
|
}
|
|
|
|
func metricSeriesToProto(series traceqlmetrics.MetricSeries) []*tempopb.KeyValue {
|
|
var r []*tempopb.KeyValue
|
|
for _, kv := range series {
|
|
if kv.Key != "" {
|
|
r = append(r, traceQLStaticToProto(&kv))
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
func traceQLStaticToProto(kv *traceqlmetrics.KeyValue) *tempopb.KeyValue {
|
|
val := tempopb.TraceQLStatic{Type: int32(kv.Value.Type)}
|
|
|
|
switch kv.Value.Type {
|
|
case traceql.TypeInt:
|
|
n, _ := kv.Value.Int()
|
|
val.N = int64(n)
|
|
case traceql.TypeFloat:
|
|
val.F = kv.Value.Float()
|
|
case traceql.TypeString:
|
|
val.S = kv.Value.EncodeToString(false)
|
|
case traceql.TypeBoolean:
|
|
b, _ := kv.Value.Bool()
|
|
val.B = b
|
|
case traceql.TypeDuration:
|
|
d, _ := kv.Value.Duration()
|
|
val.D = uint64(d)
|
|
case traceql.TypeStatus:
|
|
st, _ := kv.Value.Status()
|
|
val.Status = int32(st)
|
|
case traceql.TypeKind:
|
|
k, _ := kv.Value.Kind()
|
|
val.Kind = int32(k)
|
|
default:
|
|
val = tempopb.TraceQLStatic{Type: int32(traceql.TypeNil)}
|
|
}
|
|
|
|
return &tempopb.KeyValue{Key: kv.Key, Value: &val}
|
|
}
|
|
|
|
// filterBatches to only root spans or kind==server. Does not modify the input
|
|
// but returns a new struct referencing the same input pointers. Returns nil
|
|
// if there were no matching spans.
|
|
func filterBatches(batches []*v1.ResourceSpans) []*v1.ResourceSpans {
|
|
keep := make([]*v1.ResourceSpans, 0, len(batches))
|
|
|
|
for _, batch := range batches {
|
|
var keepSS []*v1.ScopeSpans
|
|
for _, ss := range batch.ScopeSpans {
|
|
|
|
var keepSpans []*v1.Span
|
|
for _, s := range ss.Spans {
|
|
if s.Kind == v1.Span_SPAN_KIND_SERVER || len(s.ParentSpanId) == 0 {
|
|
keepSpans = append(keepSpans, s)
|
|
}
|
|
}
|
|
|
|
if len(keepSpans) > 0 {
|
|
keepSS = append(keepSS, &v1.ScopeSpans{
|
|
Scope: ss.Scope,
|
|
Spans: keepSpans,
|
|
})
|
|
}
|
|
}
|
|
|
|
if len(keepSS) > 0 {
|
|
keep = append(keep, &v1.ResourceSpans{
|
|
Resource: batch.Resource,
|
|
ScopeSpans: keepSS,
|
|
})
|
|
}
|
|
}
|
|
|
|
return keep
|
|
}
|
|
|
|
type flushOp struct {
|
|
blockID uuid.UUID
|
|
at time.Time // When to execute
|
|
attempts int
|
|
bo time.Duration
|
|
}
|
|
|
|
func newFlushOp(blockID uuid.UUID) *flushOp {
|
|
return &flushOp{
|
|
blockID: blockID,
|
|
at: time.Now(),
|
|
bo: 30 * time.Second,
|
|
}
|
|
}
|
|
|
|
func (f *flushOp) Key() string { return f.blockID.String() }
|
|
|
|
func (f *flushOp) Priority() int64 { return -f.at.Unix() }
|
|
|
|
const maxBackoff = 5 * time.Minute
|
|
|
|
func (f *flushOp) backoff() time.Duration {
|
|
f.bo *= 2
|
|
if f.bo > maxBackoff {
|
|
f.bo = maxBackoff
|
|
}
|
|
|
|
return f.bo
|
|
}
|