@@ -560,14 +560,50 @@ func (r *OpenAIRouter) handleModelRouting(openAIRequest *openai.ChatCompletionNe
560560 }
561561
562562 if classificationText != "" {
563+ // Start classification span
564+ classifyCtx , classifySpan := observability .StartSpan (ctx .TraceContext , observability .SpanClassification )
565+ classifyStart := time .Now ()
566+
563567 // Find the most similar task description or classify, then select best model
564568 matchedModel := r .classifyAndSelectBestModel (classificationText )
569+ classifyTime := time .Since (classifyStart ).Milliseconds ()
570+
571+ // Get category information for the span
572+ categoryName := r .findCategoryForClassification (classificationText )
573+
574+ observability .SetSpanAttributes (classifySpan ,
575+ attribute .String (observability .AttrCategoryName , categoryName ),
576+ attribute .String (observability .AttrClassifierType , "bert" ),
577+ attribute .Int64 (observability .AttrClassificationTimeMs , classifyTime ))
578+ classifySpan .End ()
579+ ctx .TraceContext = classifyCtx
580+
565581 if matchedModel != originalModel && matchedModel != "" {
566- // Get detected PII for policy checking
582+ // Start PII detection span if enabled
567583 allContent := pii .ExtractAllContent (userContent , nonUserMessages )
568584 if r .PIIChecker .IsPIIEnabled (matchedModel ) {
585+ piiCtx , piiSpan := observability .StartSpan (ctx .TraceContext , observability .SpanPIIDetection )
586+ piiStart := time .Now ()
587+
569588 observability .Infof ("PII policy enabled for model %s" , matchedModel )
570589 detectedPII := r .Classifier .DetectPIIInContent (allContent )
590+
591+ piiTime := time .Since (piiStart ).Milliseconds ()
592+ piiDetected := len (detectedPII ) > 0
593+
594+ observability .SetSpanAttributes (piiSpan ,
595+ attribute .Bool (observability .AttrPIIDetected , piiDetected ),
596+ attribute .Int64 (observability .AttrPIIDetectionTimeMs , piiTime ))
597+
598+ if piiDetected {
599+ // Convert detected PII to comma-separated string
600+ piiTypesStr := strings .Join (detectedPII , "," )
601+ observability .SetSpanAttributes (piiSpan ,
602+ attribute .String (observability .AttrPIITypes , piiTypesStr ))
603+ }
604+
605+ piiSpan .End ()
606+ ctx .TraceContext = piiCtx
571607
572608 // Check if the initially selected model passes PII policy
573609 allowed , deniedPII , err := r .PIIChecker .CheckPolicy (matchedModel , detectedPII )
@@ -622,6 +658,9 @@ func (r *OpenAIRouter) handleModelRouting(openAIRequest *openai.ChatCompletionNe
622658
623659 observability .Infof ("Routing to model: %s" , matchedModel )
624660
661+ // Start routing decision span
662+ routingCtx , routingSpan := observability .StartSpan (ctx .TraceContext , observability .SpanRoutingDecision )
663+
625664 // Check reasoning mode for this category using entropy-based approach
626665 useReasoning , categoryName , reasoningDecision := r .getEntropyBasedReasoningModeAndCategory (userContent )
627666 observability .Infof ("Entropy-based reasoning decision for this query: %v on [%s] model (confidence: %.3f, reason: %s)" ,
@@ -630,6 +669,18 @@ func (r *OpenAIRouter) handleModelRouting(openAIRequest *openai.ChatCompletionNe
630669 effortForMetrics := r .getReasoningEffort (categoryName )
631670 metrics .RecordReasoningDecision (categoryName , matchedModel , useReasoning , effortForMetrics )
632671
672+ // Set routing attributes on span
673+ observability .SetSpanAttributes (routingSpan ,
674+ attribute .String (observability .AttrRoutingStrategy , "auto" ),
675+ attribute .String (observability .AttrRoutingReason , reasoningDecision .DecisionReason ),
676+ attribute .String (observability .AttrOriginalModel , originalModel ),
677+ attribute .String (observability .AttrSelectedModel , matchedModel ),
678+ attribute .Bool (observability .AttrReasoningEnabled , useReasoning ),
679+ attribute .String (observability .AttrReasoningEffort , effortForMetrics ))
680+
681+ routingSpan .End ()
682+ ctx .TraceContext = routingCtx
683+
633684 // Track VSR decision information
634685 ctx .VSRSelectedCategory = categoryName
635686 ctx .VSRSelectedModel = matchedModel
@@ -645,14 +696,28 @@ func (r *OpenAIRouter) handleModelRouting(openAIRequest *openai.ChatCompletionNe
645696 // Update the actual model that will be used
646697 actualModel = matchedModel
647698
699+ // Start backend selection span
700+ backendCtx , backendSpan := observability .StartSpan (ctx .TraceContext , observability .SpanBackendSelection )
701+
648702 // Select the best endpoint for this model
649703 endpointAddress , endpointFound := r .Config .SelectBestEndpointAddressForModel (matchedModel )
650704 if endpointFound {
651705 selectedEndpoint = endpointAddress
652706 observability .Infof ("Selected endpoint address: %s for model: %s" , selectedEndpoint , matchedModel )
707+
708+ // Extract endpoint name from config
709+ endpoints := r .Config .GetEndpointsForModel (matchedModel )
710+ if len (endpoints ) > 0 {
711+ observability .SetSpanAttributes (backendSpan ,
712+ attribute .String (observability .AttrEndpointName , endpoints [0 ].Name ),
713+ attribute .String (observability .AttrEndpointAddress , selectedEndpoint ))
714+ }
653715 } else {
654716 observability .Warnf ("No endpoint found for model %s, using fallback" , matchedModel )
655717 }
718+
719+ backendSpan .End ()
720+ ctx .TraceContext = backendCtx
656721
657722 // Modify the model in the request
658723 openAIRequest .Model = openai .ChatModel (matchedModel )
@@ -688,21 +753,35 @@ func (r *OpenAIRouter) handleModelRouting(openAIRequest *openai.ChatCompletionNe
688753 }
689754
690755 if category != nil && category .SystemPrompt != "" && category .IsSystemPromptEnabled () {
756+ // Start system prompt injection span
757+ promptCtx , promptSpan := observability .StartSpan (ctx .TraceContext , observability .SpanSystemPromptInjection )
758+
691759 mode := category .GetSystemPromptMode ()
692760 var injected bool
693761 modifiedBody , injected , err = addSystemPromptToRequestBody (modifiedBody , category .SystemPrompt , mode )
694762 if err != nil {
695763 observability .Errorf ("Error adding system prompt to request: %v" , err )
764+ observability .RecordError (promptSpan , err )
696765 metrics .RecordRequestError (actualModel , "serialization_error" )
766+ promptSpan .End ()
697767 return nil , status .Errorf (codes .Internal , "error adding system prompt: %v" , err )
698768 }
769+
770+ observability .SetSpanAttributes (promptSpan ,
771+ attribute .Bool ("system_prompt.injected" , injected ),
772+ attribute .String ("system_prompt.mode" , mode ),
773+ attribute .String (observability .AttrCategoryName , categoryName ))
774+
699775 if injected {
700776 ctx .VSRInjectedSystemPrompt = true
701777 observability .Infof ("Added category-specific system prompt for category: %s (mode: %s)" , categoryName , mode )
702778 }
703779
704780 // Log metadata about system prompt injection (avoid logging sensitive user data)
705781 observability .Infof ("System prompt injection completed for category: %s, body size: %d bytes" , categoryName , len (modifiedBody ))
782+
783+ promptSpan .End ()
784+ ctx .TraceContext = promptCtx
706785 } else if category != nil && category .SystemPrompt != "" && ! category .IsSystemPromptEnabled () {
707786 observability .Infof ("System prompt disabled for category: %s" , categoryName )
708787 }
0 commit comments