253 lines
6.8 KiB
Go
253 lines
6.8 KiB
Go
package tracing
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/maximhq/bifrost/core/schemas"
|
|
)
|
|
|
|
func TestCreateTrace_WithInheritedTraceID(t *testing.T) {
|
|
store := NewTraceStore(5*time.Minute, nil)
|
|
defer store.Stop()
|
|
|
|
// Use a trace ID from an incoming W3C traceparent header
|
|
inheritedTraceID := "69538b980000000079943934f90c1d40"
|
|
|
|
traceID := store.CreateTrace(inheritedTraceID)
|
|
|
|
if traceID != inheritedTraceID {
|
|
t.Errorf("CreateTrace() returned %q, want inherited trace ID %q", traceID, inheritedTraceID)
|
|
}
|
|
|
|
trace := store.GetTrace(traceID)
|
|
if trace == nil {
|
|
t.Fatal("GetTrace() returned nil")
|
|
}
|
|
|
|
if trace.TraceID != inheritedTraceID {
|
|
t.Errorf("trace.TraceID = %q, want %q", trace.TraceID, inheritedTraceID)
|
|
}
|
|
|
|
// ParentID should be empty - we no longer set it incorrectly to the trace ID
|
|
if trace.ParentID != "" {
|
|
t.Errorf("trace.ParentID = %q, want empty string (parent span ID is set on spans, not traces)", trace.ParentID)
|
|
}
|
|
}
|
|
|
|
func TestCreateTrace_GeneratesNewTraceID(t *testing.T) {
|
|
store := NewTraceStore(5*time.Minute, nil)
|
|
defer store.Stop()
|
|
|
|
traceID := store.CreateTrace("")
|
|
|
|
if traceID == "" {
|
|
t.Error("CreateTrace() returned empty trace ID")
|
|
}
|
|
|
|
// Generated trace ID should be 32 hex characters
|
|
if len(traceID) != 32 {
|
|
t.Errorf("Generated trace ID length = %d, want 32", len(traceID))
|
|
}
|
|
|
|
// Verify it's valid hex
|
|
if !isHex(traceID) {
|
|
t.Errorf("Generated trace ID %q is not valid hex", traceID)
|
|
}
|
|
|
|
trace := store.GetTrace(traceID)
|
|
if trace == nil {
|
|
t.Fatal("GetTrace() returned nil")
|
|
}
|
|
|
|
if trace.ParentID != "" {
|
|
t.Errorf("trace.ParentID = %q, want empty string", trace.ParentID)
|
|
}
|
|
}
|
|
|
|
func TestStartSpan_RootSpanHasNoParent(t *testing.T) {
|
|
store := NewTraceStore(5*time.Minute, nil)
|
|
defer store.Stop()
|
|
|
|
traceID := store.CreateTrace("")
|
|
|
|
span := store.StartSpan(traceID, "root-operation", schemas.SpanKindHTTPRequest)
|
|
if span == nil {
|
|
t.Fatal("StartSpan() returned nil")
|
|
}
|
|
|
|
// Root span should have no parent when there's no incoming trace context
|
|
if span.ParentID != "" {
|
|
t.Errorf("root span.ParentID = %q, want empty string", span.ParentID)
|
|
}
|
|
|
|
if span.TraceID != traceID {
|
|
t.Errorf("span.TraceID = %q, want %q", span.TraceID, traceID)
|
|
}
|
|
|
|
// Verify it's set as root span
|
|
trace := store.GetTrace(traceID)
|
|
if trace.RootSpan != span {
|
|
t.Error("StartSpan() did not set trace.RootSpan")
|
|
}
|
|
}
|
|
|
|
func TestStartSpan_SecondSpanHasRootAsParent(t *testing.T) {
|
|
store := NewTraceStore(5*time.Minute, nil)
|
|
defer store.Stop()
|
|
|
|
traceID := store.CreateTrace("")
|
|
|
|
rootSpan := store.StartSpan(traceID, "root-operation", schemas.SpanKindHTTPRequest)
|
|
if rootSpan == nil {
|
|
t.Fatal("StartSpan() returned nil for root span")
|
|
}
|
|
|
|
// Second span created with StartSpan should have root as parent
|
|
secondSpan := store.StartSpan(traceID, "second-operation", schemas.SpanKindLLMCall)
|
|
if secondSpan == nil {
|
|
t.Fatal("StartSpan() returned nil for second span")
|
|
}
|
|
|
|
if secondSpan.ParentID != rootSpan.SpanID {
|
|
t.Errorf("second span.ParentID = %q, want root span ID %q", secondSpan.ParentID, rootSpan.SpanID)
|
|
}
|
|
}
|
|
|
|
func TestStartChildSpan_HasCorrectParent(t *testing.T) {
|
|
store := NewTraceStore(5*time.Minute, nil)
|
|
defer store.Stop()
|
|
|
|
traceID := store.CreateTrace("")
|
|
|
|
rootSpan := store.StartSpan(traceID, "root-operation", schemas.SpanKindHTTPRequest)
|
|
if rootSpan == nil {
|
|
t.Fatal("StartSpan() returned nil for root span")
|
|
}
|
|
|
|
// Create a child span with explicit parent
|
|
childSpan := store.StartChildSpan(traceID, rootSpan.SpanID, "child-operation", schemas.SpanKindLLMCall)
|
|
if childSpan == nil {
|
|
t.Fatal("StartChildSpan() returned nil")
|
|
}
|
|
|
|
if childSpan.ParentID != rootSpan.SpanID {
|
|
t.Errorf("child span.ParentID = %q, want %q", childSpan.ParentID, rootSpan.SpanID)
|
|
}
|
|
|
|
if childSpan.TraceID != traceID {
|
|
t.Errorf("child span.TraceID = %q, want %q", childSpan.TraceID, traceID)
|
|
}
|
|
}
|
|
|
|
func TestStartChildSpan_WithExternalParentSpanID(t *testing.T) {
|
|
store := NewTraceStore(5*time.Minute, nil)
|
|
defer store.Stop()
|
|
|
|
// Simulating an incoming request with W3C traceparent header
|
|
inheritedTraceID := "69538b980000000079943934f90c1d40"
|
|
externalParentSpanID := "aad09d1659b4c7e3" // Parent span ID from upstream service
|
|
|
|
traceID := store.CreateTrace(inheritedTraceID)
|
|
|
|
// Create root span as child of external parent span
|
|
// This is what should happen when processing an incoming distributed trace
|
|
rootSpan := store.StartChildSpan(traceID, externalParentSpanID, "bifrost-request", schemas.SpanKindHTTPRequest)
|
|
if rootSpan == nil {
|
|
t.Fatal("StartChildSpan() returned nil")
|
|
}
|
|
|
|
// Root span should have the external parent span ID
|
|
if rootSpan.ParentID != externalParentSpanID {
|
|
t.Errorf("root span.ParentID = %q, want external parent %q", rootSpan.ParentID, externalParentSpanID)
|
|
}
|
|
|
|
if rootSpan.TraceID != inheritedTraceID {
|
|
t.Errorf("root span.TraceID = %q, want inherited trace ID %q", rootSpan.TraceID, inheritedTraceID)
|
|
}
|
|
}
|
|
|
|
func TestGetTrace_NotFound(t *testing.T) {
|
|
store := NewTraceStore(5*time.Minute, nil)
|
|
defer store.Stop()
|
|
|
|
trace := store.GetTrace("nonexistent-trace-id")
|
|
if trace != nil {
|
|
t.Error("GetTrace() should return nil for nonexistent trace")
|
|
}
|
|
}
|
|
|
|
func TestCompleteTrace_ReturnsAndRemoves(t *testing.T) {
|
|
store := NewTraceStore(5*time.Minute, nil)
|
|
defer store.Stop()
|
|
|
|
traceID := store.CreateTrace("")
|
|
store.StartSpan(traceID, "operation", schemas.SpanKindHTTPRequest)
|
|
|
|
trace := store.CompleteTrace(traceID)
|
|
if trace == nil {
|
|
t.Fatal("CompleteTrace() returned nil")
|
|
}
|
|
|
|
if trace.TraceID != traceID {
|
|
t.Errorf("trace.TraceID = %q, want %q", trace.TraceID, traceID)
|
|
}
|
|
|
|
if trace.EndTime.IsZero() {
|
|
t.Error("trace.EndTime should be set")
|
|
}
|
|
|
|
// Trace should be removed from store
|
|
if store.GetTrace(traceID) != nil {
|
|
t.Error("Trace should be removed from store after CompleteTrace()")
|
|
}
|
|
}
|
|
|
|
func TestEndSpan_SetsStatusAndTime(t *testing.T) {
|
|
store := NewTraceStore(5*time.Minute, nil)
|
|
defer store.Stop()
|
|
|
|
traceID := store.CreateTrace("")
|
|
span := store.StartSpan(traceID, "operation", schemas.SpanKindHTTPRequest)
|
|
|
|
store.EndSpan(traceID, span.SpanID, schemas.SpanStatusOk, "success", map[string]any{
|
|
"custom.attr": "value",
|
|
})
|
|
|
|
if span.Status != schemas.SpanStatusOk {
|
|
t.Errorf("span.Status = %v, want SpanStatusOk", span.Status)
|
|
}
|
|
|
|
if span.EndTime.IsZero() {
|
|
t.Error("span.EndTime should be set")
|
|
}
|
|
|
|
if span.Attributes["custom.attr"] != "value" {
|
|
t.Error("EndSpan() should set custom attributes")
|
|
}
|
|
}
|
|
|
|
func TestGenerateTraceID_Format(t *testing.T) {
|
|
id := generateTraceID()
|
|
|
|
if len(id) != 32 {
|
|
t.Errorf("generateTraceID() length = %d, want 32", len(id))
|
|
}
|
|
|
|
if !isHex(id) {
|
|
t.Errorf("generateTraceID() = %q, not valid hex", id)
|
|
}
|
|
}
|
|
|
|
func TestGenerateSpanID_Format(t *testing.T) {
|
|
id := generateSpanID()
|
|
|
|
if len(id) != 16 {
|
|
t.Errorf("generateSpanID() length = %d, want 16", len(id))
|
|
}
|
|
|
|
if !isHex(id) {
|
|
t.Errorf("generateSpanID() = %q, not valid hex", id)
|
|
}
|
|
}
|