From bba10939d0d57cccab2eb1597bf25900a0d7161d Mon Sep 17 00:00:00 2001 From: roroghost17 Date: Fri, 21 Aug 2026 14:00:01 +0530 Subject: [PATCH] chore: remove legacy metrics in connectors --- core/bifrost.go | 21 +--------- core/providers/utils/utils.go | 1 - core/schemas/trace.go | 65 +++++++----------------------- framework/tracing/llmspan.go | 35 ---------------- plugins/otel/converter.go | 1 - plugins/otel/entityset_test.go | 2 +- plugins/otel/main.go | 33 ++++++--------- plugins/telemetry/main.go | 10 +---- plugins/telemetry/main_test.go | 63 +++++++++++++++++++++++++++++ plugins/telemetry/utils.go | 13 +----- transports/bifrost-http/lib/ctx.go | 9 +++-- 11 files changed, 101 insertions(+), 152 deletions(-) diff --git a/core/bifrost.go b/core/bifrost.go index ae392c87b2b..45bf65e6c60 100644 --- a/core/bifrost.go +++ b/core/bifrost.go @@ -6121,7 +6121,7 @@ func executeRequestWithRetries[T any]( keyTracer.SetAttribute(keyHandle, schemas.AttrBifrostProviderName, string(providerKey)) // raw Bifrost short name, mirrors canonical gen_ai.provider.name keyTracer.SetAttribute(keyHandle, schemas.AttrRequestModel, model) if attempts > 0 { - keyTracer.SetAttribute(keyHandle, schemas.AttrLegacyRetryCount, attempts) + keyTracer.SetAttribute(keyHandle, schemas.AttrBifrostRetries, attempts) } } @@ -6293,46 +6293,31 @@ func executeRequestWithRetries[T any]( span.SetAttribute(schemas.AttrBifrostProviderName, string(providerKey)) // raw Bifrost short name, mirrors canonical gen_ai.provider.name span.SetAttribute(schemas.AttrRequestModel, model) span.SetAttribute(schemas.AttrOperationName, otelOp) - span.SetAttribute(schemas.AttrLegacyRequestType, string(requestType)) // legacy: replaced by gen_ai.operation.name - if attempts > 0 { - span.SetAttribute(schemas.AttrLegacyRetryCount, attempts) // legacy: bare key with no semconv prefix - } + span.SetAttribute(schemas.AttrLegacyRequestType, string(requestType)) // Add context-related attributes (selected key, virtual key, team, customer, etc.) - // Each AttrXxx (gen_ai.*) emission below is LEGACY namespace pollution: the - // Bifrost-internal concept does not belong under gen_ai.*. The bifrost.* mirrors - // are the canonical home going forward; once all dashboards migrate, drop the - // gen_ai.* lines (grep for "// legacy:" in this block). if selectedKeyID, ok := ctx.Value(schemas.BifrostContextKeySelectedKeyID).(string); ok && selectedKeyID != "" { - span.SetAttribute(schemas.AttrSelectedKeyID, selectedKeyID) // legacy: gen_ai.* placement of bifrost-internal attr span.SetAttribute(schemas.AttrBifrostSelectedKeyID, selectedKeyID) } if selectedKeyName, ok := ctx.Value(schemas.BifrostContextKeySelectedKeyName).(string); ok && selectedKeyName != "" { - span.SetAttribute(schemas.AttrSelectedKeyName, selectedKeyName) // legacy: gen_ai.* placement of bifrost-internal attr span.SetAttribute(schemas.AttrBifrostSelectedKeyName, selectedKeyName) } if virtualKeyID, ok := ctx.Value(schemas.BifrostContextKeyGovernanceVirtualKeyID).(string); ok && virtualKeyID != "" { - span.SetAttribute(schemas.AttrVirtualKeyID, virtualKeyID) // legacy: gen_ai.* placement of bifrost-internal attr span.SetAttribute(schemas.AttrBifrostVirtualKeyID, virtualKeyID) } if virtualKeyName, ok := ctx.Value(schemas.BifrostContextKeyGovernanceVirtualKeyName).(string); ok && virtualKeyName != "" { - span.SetAttribute(schemas.AttrVirtualKeyName, virtualKeyName) // legacy: gen_ai.* placement of bifrost-internal attr span.SetAttribute(schemas.AttrBifrostVirtualKeyName, virtualKeyName) } if teamID, ok := ctx.Value(schemas.BifrostContextKeyGovernanceTeamID).(string); ok && teamID != "" { - span.SetAttribute(schemas.AttrTeamID, teamID) // legacy: gen_ai.* placement of bifrost-internal attr span.SetAttribute(schemas.AttrBifrostTeamID, teamID) } if teamName, ok := ctx.Value(schemas.BifrostContextKeyGovernanceTeamName).(string); ok && teamName != "" { - span.SetAttribute(schemas.AttrTeamName, teamName) // legacy: gen_ai.* placement of bifrost-internal attr span.SetAttribute(schemas.AttrBifrostTeamName, teamName) } if customerID, ok := ctx.Value(schemas.BifrostContextKeyGovernanceCustomerID).(string); ok && customerID != "" { - span.SetAttribute(schemas.AttrCustomerID, customerID) // legacy: gen_ai.* placement of bifrost-internal attr span.SetAttribute(schemas.AttrBifrostCustomerID, customerID) } if customerName, ok := ctx.Value(schemas.BifrostContextKeyGovernanceCustomerName).(string); ok && customerName != "" { - span.SetAttribute(schemas.AttrCustomerName, customerName) // legacy: gen_ai.* placement of bifrost-internal attr span.SetAttribute(schemas.AttrBifrostCustomerName, customerName) } if businessUnitID, ok := ctx.Value(schemas.BifrostContextKeyGovernanceBusinessUnitID).(string); ok && businessUnitID != "" { @@ -6369,10 +6354,8 @@ func executeRequestWithRetries[T any]( span.SetAttribute(schemas.AttrBifrostUserEmail, userEmail) } if fallbackIndex, ok := ctx.Value(schemas.BifrostContextKeyFallbackIndex).(int); ok { - span.SetAttribute(schemas.AttrFallbackIndex, fallbackIndex) // legacy: gen_ai.* placement of bifrost-internal attr span.SetAttribute(schemas.AttrBifrostFallbackIndex, fallbackIndex) } - span.SetAttribute(schemas.AttrNumberOfRetries, attempts) // legacy: gen_ai.* placement of bifrost-internal attr span.SetAttribute(schemas.AttrBifrostRetries, attempts) // Surface caller-supplied extra headers (from x-bf-eh-* and direct-allowlist diff --git a/core/providers/utils/utils.go b/core/providers/utils/utils.go index d3fc2bc847c..27d30f3b2ef 100644 --- a/core/providers/utils/utils.go +++ b/core/providers/utils/utils.go @@ -3830,7 +3830,6 @@ func completeDeferredSpan(ctx *schemas.BifrostContext, result *schemas.BifrostRe // Set TTFT and chunk count attributes regardless of accumulated response availability // (GetAccumulatedChunks may return nil response while still providing valid metrics) if ttftNs > 0 { - tracer.SetAttribute(handle, schemas.AttrTimeToFirstToken, ttftNs) // legacy: nanoseconds; replaced by gen_ai.response.time_to_first_chunk tracer.SetAttribute(handle, schemas.AttrTimeToFirstChunk, float64(ttftNs)/1e9) // spec: seconds } if chunkCount > 0 { diff --git a/core/schemas/trace.go b/core/schemas/trace.go index f440d62867f..61d6d207f86 100644 --- a/core/schemas/trace.go +++ b/core/schemas/trace.go @@ -710,15 +710,11 @@ const ( AttrEcho = "gen_ai.request.echo" AttrLogitBias = "gen_ai.request.logit_bias" AttrLogProbs = "gen_ai.request.logprobs" - AttrN = "gen_ai.request.n" // legacy: replaced by AttrChoiceCount AttrChoiceCount = "gen_ai.request.choice.count" - // AttrEmbeddingsDimensionCount is the OTel spec key for embedding dimensions - // (Bifrost historically emitted AttrDimensions = gen_ai.request.dimensions). + // AttrEmbeddingsDimensionCount is the OTel spec key for embedding dimensions. AttrEmbeddingsDimensionCount = "gen_ai.embeddings.dimension.count" AttrSeed = "gen_ai.request.seed" AttrSuffix = "gen_ai.request.suffix" - AttrDimensions = "gen_ai.request.dimensions" // legacy: replaced by AttrEmbeddingsDimensionCount - AttrEncodingFormat = "gen_ai.request.encoding_format" // legacy: singular form; replaced by AttrEncodingFormats (string[]) AttrEncodingFormats = "gen_ai.request.encoding_formats" AttrLanguage = "gen_ai.request.language" AttrPrompt = "gen_ai.request.prompt" @@ -739,7 +735,6 @@ const ( AttrServiceTier = "gen_ai.response.service_tier" AttrCreated = "gen_ai.response.created" AttrObject = "gen_ai.response.object" - AttrTimeToFirstToken = "gen_ai.response.time_to_first_token" // legacy: nanoseconds; replaced by gen_ai.response.time_to_first_chunk (seconds) AttrTimeToFirstChunk = "gen_ai.response.time_to_first_chunk" AttrTotalChunks = "gen_ai.response.total_chunks" @@ -750,28 +745,21 @@ const ( AttrPluginErrorCount = "plugin.error_count" // Usage Attributes - // legacy: AttrPromptTokens / AttrCompletionTokens are the deprecated OTel names; - // new code should use AttrInputTokens / AttrOutputTokens. Kept for dashboards. - AttrPromptTokens = "gen_ai.usage.prompt_tokens" - AttrCompletionTokens = "gen_ai.usage.completion_tokens" - AttrTotalTokens = "gen_ai.usage.total_tokens" - AttrInputTokens = "gen_ai.usage.input_tokens" - AttrOutputTokens = "gen_ai.usage.output_tokens" - AttrUsageCost = "gen_ai.usage.cost" + AttrTotalTokens = "gen_ai.usage.total_tokens" + AttrInputTokens = "gen_ai.usage.input_tokens" + AttrOutputTokens = "gen_ai.usage.output_tokens" + AttrUsageCost = "gen_ai.usage.cost" // OTel GenAI spec keys for cache tokens (flat namespace). AttrUsageCacheReadInputTokens = "gen_ai.usage.cache_read.input_tokens" AttrUsageCacheCreationInputTokens = "gen_ai.usage.cache_creation.input_tokens" // OTel GenAI spec key for reasoning tokens (flat namespace). AttrUsageReasoningOutputTokens = "gen_ai.usage.reasoning.output_tokens" - // Chat completion usage detail attributes - // legacy: nested namespace; OTel spec uses flat gen_ai.usage.cache_read.input_tokens - // and gen_ai.usage.cache_creation.input_tokens for the cached_* entries. The - // non-cached fields below have no spec equivalent and stay as-is. + // Chat completion usage detail attributes. These non-cached fields have no OTel + // spec equivalent and stay as-is; cache tokens use the flat gen_ai.usage.cache_* + // keys (AttrUsageCacheReadInputTokens / AttrUsageCacheCreationInputTokens). AttrPromptTokenDetailsText = "gen_ai.usage.prompt_token_details.text_tokens" AttrPromptTokenDetailsAudio = "gen_ai.usage.prompt_token_details.audio_tokens" AttrPromptTokenDetailsImage = "gen_ai.usage.prompt_token_details.image_tokens" - AttrPromptTokenDetailsCachedRead = "gen_ai.usage.prompt_token_details.cached_read_tokens" // legacy: see AttrUsageCacheReadInputTokens - AttrPromptTokenDetailsCachedWrite = "gen_ai.usage.prompt_token_details.cached_write_tokens" // legacy: see AttrUsageCacheCreationInputTokens AttrPromptTokenDetailsCachedWrite5m = "gen_ai.usage.prompt_token_details.cached_write_tokens_5m" AttrPromptTokenDetailsCachedWrite1h = "gen_ai.usage.prompt_token_details.cached_write_tokens_1h" AttrCompletionTokenDetailsText = "gen_ai.usage.completion_token_details.text_tokens" @@ -784,10 +772,7 @@ const ( AttrCompletionTokenDetailsSearch = "gen_ai.usage.completion_token_details.num_search_queries" // Error Attributes - AttrError = "gen_ai.error" - // legacy: AttrErrorType is the gen_ai.* placement; OTel general semconv uses the - // unprefixed "error.type". Emitted in parallel from PopulateErrorAttributes. - AttrErrorType = "gen_ai.error.type" + AttrError = "gen_ai.error" AttrErrorCode = "gen_ai.error.code" // AttrHTTPResponseStatusCode is the OTel semconv HTTP response status code (e.g. 400). // Sourced from BifrostError.StatusCode; used as the status_code dimension on error metrics. @@ -800,24 +785,6 @@ const ( AttrInputEmbedding = "gen_ai.input.embedding" AttrOutputMessages = "gen_ai.output.messages" - // Bifrost Context Attributes - // legacy: every key below sits under gen_ai.* but represents a Bifrost-internal - // concept (governance / routing). The bifrost.* mirrors are the canonical home - // going forward; these will be dropped once dashboards migrate. - AttrRequestID = "gen_ai.request_id" - AttrVirtualKeyID = "gen_ai.virtual_key_id" - AttrVirtualKeyName = "gen_ai.virtual_key_name" - AttrSelectedKeyID = "gen_ai.selected_key_id" - AttrSelectedKeyName = "gen_ai.selected_key_name" - AttrRoutingRuleID = "gen_ai.routing_rule_id" - AttrRoutingRuleName = "gen_ai.routing_rule_name" - AttrTeamID = "gen_ai.team_id" - AttrTeamName = "gen_ai.team_name" - AttrCustomerID = "gen_ai.customer_id" - AttrCustomerName = "gen_ai.customer_name" - AttrNumberOfRetries = "gen_ai.number_of_retries" - AttrFallbackIndex = "gen_ai.fallback_index" - // Extra Header Attributes AttrExtraHeaderPrefix = "gen_ai.request.extra_header." @@ -936,9 +903,6 @@ const ( // - Bifrost-internal concepts (routing/governance, request id, retry counters) // - Raw Bifrost short names that mirror canonicalized gen_ai.* values // - Back-compat fallbacks for shape changes (e.g. comma-joined stop_sequences) - // - // The corresponding legacy gen_ai.* emissions are tagged "// legacy:" at their - // call sites and will be removed once dashboards migrate over. // ===================================================================== // Cumulative time (float64 ms) the request spent blocked on sockets outside // Bifrost — every provider attempt, plus MCP tool calls and media fetches. @@ -983,15 +947,14 @@ const ( AttrBifrostRoutingEngineUsed = "bifrost.routing_engine_used" // comma-joined routing engines that handled the request AttrBifrostStopSequencesJoined = "bifrost.request.stop_sequences" - // OTel general semconv (no gen_ai prefix). Emitted alongside the legacy - // gen_ai.error.type from PopulateErrorAttributes. + // OTel general semconv (no gen_ai prefix). The canonical error-type key, + // emitted from PopulateErrorAttributes. AttrErrorTypeSpec = "error.type" - // legacy: bare unprefixed keys retained for back-compat with existing dashboards. - // "request.type" is superseded by AttrOperationName; "retry.count" has no spec - // equivalent but stays under bifrost.retries going forward. + // legacy: bare unprefixed key retained for back-compat with existing dashboards. + // "request.type" is superseded by AttrOperationName, but still drives the live + // "method" metric label and request_type column via EnrichmentDims. AttrLegacyRequestType = "request.type" - AttrLegacyRetryCount = "retry.count" // File Operation Attributes AttrFileID = "gen_ai.file.id" diff --git a/framework/tracing/llmspan.go b/framework/tracing/llmspan.go index 5f0561e806f..a61afbe59c2 100644 --- a/framework/tracing/llmspan.go +++ b/framework/tracing/llmspan.go @@ -139,7 +139,6 @@ func PopulateErrorAttributes(err *schemas.BifrostError) map[string]any { attrs[schemas.AttrError] = err.Error.Message if err.Error.Type != nil { - attrs[schemas.AttrErrorType] = *err.Error.Type // legacy: gen_ai.error.type; spec uses the unprefixed error.type attrs[schemas.AttrErrorTypeSpec] = *err.Error.Type } if err.Error.Code != nil { @@ -164,42 +163,28 @@ func PopulateContextAttributes( userID, userName, userEmail string, numberOfRetries, fallbackIndex int, ) { - // Each AttrXxx (gen_ai.*) emission below is LEGACY namespace pollution: a - // Bifrost-internal concept does not belong under gen_ai.*. The bifrost.* mirrors - // are the canonical home going forward; drop the gen_ai.* lines once dashboards - // migrate (grep for "// legacy:" inside this function). if virtualKeyID != "" { - attrs[schemas.AttrVirtualKeyID] = virtualKeyID // legacy: gen_ai.* placement of bifrost-internal attr - attrs[schemas.AttrVirtualKeyName] = virtualKeyName // legacy: gen_ai.* placement of bifrost-internal attr attrs[schemas.AttrBifrostVirtualKeyID] = virtualKeyID attrs[schemas.AttrBifrostVirtualKeyName] = virtualKeyName } if selectedKeyID != "" { - attrs[schemas.AttrSelectedKeyID] = selectedKeyID // legacy: gen_ai.* placement of bifrost-internal attr - attrs[schemas.AttrSelectedKeyName] = selectedKeyName // legacy: gen_ai.* placement of bifrost-internal attr attrs[schemas.AttrBifrostSelectedKeyID] = selectedKeyID attrs[schemas.AttrBifrostSelectedKeyName] = selectedKeyName } if routingRuleID != "" { - attrs[schemas.AttrRoutingRuleID] = routingRuleID // legacy: gen_ai.* placement of bifrost-internal attr - attrs[schemas.AttrRoutingRuleName] = routingRuleName // legacy: gen_ai.* placement of bifrost-internal attr attrs[schemas.AttrBifrostRoutingRuleID] = routingRuleID attrs[schemas.AttrBifrostRoutingRuleName] = routingRuleName } if teamID != "" { - attrs[schemas.AttrTeamID] = teamID // legacy: gen_ai.* placement of bifrost-internal attr attrs[schemas.AttrBifrostTeamID] = teamID } if teamName != "" { - attrs[schemas.AttrTeamName] = teamName // legacy: gen_ai.* placement of bifrost-internal attr attrs[schemas.AttrBifrostTeamName] = teamName } if customerID != "" { - attrs[schemas.AttrCustomerID] = customerID // legacy: gen_ai.* placement of bifrost-internal attr attrs[schemas.AttrBifrostCustomerID] = customerID } if customerName != "" { - attrs[schemas.AttrCustomerName] = customerName // legacy: gen_ai.* placement of bifrost-internal attr attrs[schemas.AttrBifrostCustomerName] = customerName } if businessUnitID != "" { @@ -217,8 +202,6 @@ func PopulateContextAttributes( if userEmail != "" { attrs[schemas.AttrBifrostUserEmail] = userEmail } - attrs[schemas.AttrNumberOfRetries] = numberOfRetries // legacy: gen_ai.* placement of bifrost-internal attr - attrs[schemas.AttrFallbackIndex] = fallbackIndex // legacy: gen_ai.* placement of bifrost-internal attr attrs[schemas.AttrBifrostRetries] = numberOfRetries attrs[schemas.AttrBifrostFallbackIndex] = fallbackIndex } @@ -317,10 +300,7 @@ func PopulateChatResponseAttributes(resp *schemas.BifrostChatResponse, attrs map // Usage if resp.Usage != nil { - attrs[schemas.AttrPromptTokens] = resp.Usage.PromptTokens // legacy: deprecated OTel name; replaced by gen_ai.usage.input_tokens - attrs[schemas.AttrCompletionTokens] = resp.Usage.CompletionTokens // legacy: deprecated OTel name; replaced by gen_ai.usage.output_tokens attrs[schemas.AttrTotalTokens] = resp.Usage.TotalTokens - // Spec keys. attrs[schemas.AttrInputTokens] = resp.Usage.PromptTokens attrs[schemas.AttrOutputTokens] = resp.Usage.CompletionTokens @@ -335,11 +315,9 @@ func PopulateChatResponseAttributes(resp *schemas.BifrostChatResponse, attrs map attrs[schemas.AttrPromptTokenDetailsImage] = resp.Usage.PromptTokensDetails.ImageTokens } if resp.Usage.PromptTokensDetails.CachedReadTokens > 0 { - attrs[schemas.AttrPromptTokenDetailsCachedRead] = resp.Usage.PromptTokensDetails.CachedReadTokens // legacy: nested key; replaced by gen_ai.usage.cache_read.input_tokens attrs[schemas.AttrUsageCacheReadInputTokens] = resp.Usage.PromptTokensDetails.CachedReadTokens } if resp.Usage.PromptTokensDetails.CachedWriteTokens > 0 { - attrs[schemas.AttrPromptTokenDetailsCachedWrite] = resp.Usage.PromptTokensDetails.CachedWriteTokens // legacy: nested key; replaced by gen_ai.usage.cache_creation.input_tokens attrs[schemas.AttrUsageCacheCreationInputTokens] = resp.Usage.PromptTokensDetails.CachedWriteTokens } if d := resp.Usage.PromptTokensDetails.CachedWriteTokenDetails; d != nil { @@ -367,7 +345,6 @@ func PopulateChatResponseAttributes(resp *schemas.BifrostChatResponse, attrs map attrs[schemas.AttrCompletionTokenDetailsImage] = *resp.Usage.CompletionTokensDetails.ImageTokens } if resp.Usage.CompletionTokensDetails.ReasoningTokens > 0 { - attrs[schemas.AttrCompletionTokenDetailsReason] = resp.Usage.CompletionTokensDetails.ReasoningTokens // legacy: nested key; replaced by gen_ai.usage.reasoning.output_tokens attrs[schemas.AttrUsageReasoningOutputTokens] = resp.Usage.CompletionTokensDetails.ReasoningTokens } if resp.Usage.CompletionTokensDetails.AcceptedPredictionTokens > 0 { @@ -429,7 +406,6 @@ func PopulateTextCompletionRequestAttributes(req *schemas.BifrostTextCompletionR attrs[schemas.AttrLogProbs] = *req.Params.LogProbs } if req.Params.N != nil { - attrs[schemas.AttrN] = *req.Params.N // legacy: replaced by gen_ai.request.choice.count attrs[schemas.AttrChoiceCount] = *req.Params.N } if req.Params.Seed != nil { @@ -492,10 +468,7 @@ func PopulateTextCompletionResponseAttributes(resp *schemas.BifrostTextCompletio // Usage if resp.Usage != nil { - attrs[schemas.AttrPromptTokens] = resp.Usage.PromptTokens // legacy: deprecated OTel name; replaced by gen_ai.usage.input_tokens - attrs[schemas.AttrCompletionTokens] = resp.Usage.CompletionTokens // legacy: deprecated OTel name; replaced by gen_ai.usage.output_tokens attrs[schemas.AttrTotalTokens] = resp.Usage.TotalTokens - // Spec keys. attrs[schemas.AttrInputTokens] = resp.Usage.PromptTokens attrs[schemas.AttrOutputTokens] = resp.Usage.CompletionTokens } @@ -513,11 +486,9 @@ func PopulateEmbeddingRequestAttributes(req *schemas.BifrostEmbeddingRequest, at if req.Params != nil { if req.Params.Dimensions != nil { - attrs[schemas.AttrDimensions] = *req.Params.Dimensions // legacy: replaced by gen_ai.embeddings.dimension.count attrs[schemas.AttrEmbeddingsDimensionCount] = *req.Params.Dimensions } if req.Params.EncodingFormat != nil { - attrs[schemas.AttrEncodingFormat] = *req.Params.EncodingFormat // legacy: singular form; replaced by gen_ai.request.encoding_formats (string[]) attrs[schemas.AttrEncodingFormats] = []string{*req.Params.EncodingFormat} } // ExtraParams @@ -550,10 +521,7 @@ func PopulateEmbeddingResponseAttributes(resp *schemas.BifrostEmbeddingResponse, } // Usage if resp.Usage != nil { - attrs[schemas.AttrPromptTokens] = resp.Usage.PromptTokens // legacy: deprecated OTel name; replaced by gen_ai.usage.input_tokens - attrs[schemas.AttrCompletionTokens] = resp.Usage.CompletionTokens // legacy: deprecated OTel name; replaced by gen_ai.usage.output_tokens attrs[schemas.AttrTotalTokens] = resp.Usage.TotalTokens - // Spec keys. attrs[schemas.AttrInputTokens] = resp.Usage.PromptTokens attrs[schemas.AttrOutputTokens] = resp.Usage.CompletionTokens } @@ -896,11 +864,9 @@ func PopulateResponsesResponseAttributes(resp *schemas.BifrostResponsesResponse, attrs[schemas.AttrInputTokenDetailsImage] = d.ImageTokens } if d.CachedReadTokens > 0 { - attrs[schemas.AttrInputTokenDetailsCachedRead] = d.CachedReadTokens // legacy: nested key; replaced by gen_ai.usage.cache_read.input_tokens attrs[schemas.AttrUsageCacheReadInputTokens] = d.CachedReadTokens } if d.CachedWriteTokens > 0 { - attrs[schemas.AttrInputTokenDetailsCachedWrite] = d.CachedWriteTokens // legacy: nested key; replaced by gen_ai.usage.cache_creation.input_tokens attrs[schemas.AttrUsageCacheCreationInputTokens] = d.CachedWriteTokens } if wd := d.CachedWriteTokenDetails; wd != nil { @@ -924,7 +890,6 @@ func PopulateResponsesResponseAttributes(resp *schemas.BifrostResponsesResponse, attrs[schemas.AttrOutputTokenDetailsImage] = *d.ImageTokens } if d.ReasoningTokens > 0 { - attrs[schemas.AttrOutputTokenDetailsReason] = d.ReasoningTokens // legacy: nested key; replaced by gen_ai.usage.reasoning.output_tokens attrs[schemas.AttrUsageReasoningOutputTokens] = d.ReasoningTokens } if d.AcceptedPredictionTokens > 0 { diff --git a/plugins/otel/converter.go b/plugins/otel/converter.go index c76b7835f65..d3d4aea68d5 100644 --- a/plugins/otel/converter.go +++ b/plugins/otel/converter.go @@ -146,7 +146,6 @@ func (p *OtelPlugin) convertTraceToResourceSpan(serviceName string, trace *schem } if requestID := trace.GetRequestID(); requestID != "" { otelSpan.Attributes = append(otelSpan.Attributes, - kvStr(schemas.AttrRequestID, requestID), // legacy: gen_ai.* placement of bifrost-internal attr; replaced by bifrost.request.id kvStr(schemas.AttrBifrostRequestID, requestID), ) } diff --git a/plugins/otel/entityset_test.go b/plugins/otel/entityset_test.go index 35774c41786..fe6082dcefc 100644 --- a/plugins/otel/entityset_test.go +++ b/plugins/otel/entityset_test.go @@ -26,7 +26,7 @@ func TestEntitySetFromAttrs_MixedAnyKeepsAlignment(t *testing.T) { schemas.AttrBifrostTeamIDs: []any{"a1", 42, "c3"}, schemas.AttrBifrostTeamNames: []any{"Alpha", "Bogus", "Gamma"}, } - ids, names := entitySetFromAttrs(attrs, schemas.AttrBifrostTeamIDs, schemas.AttrBifrostTeamNames, schemas.AttrTeamID, schemas.AttrTeamName) + ids, names := entitySetFromAttrs(attrs, schemas.AttrBifrostTeamIDs, schemas.AttrBifrostTeamNames, schemas.AttrBifrostTeamID, schemas.AttrBifrostTeamName) if ids != "a1,c3" || names != "Alpha,Gamma" { t.Errorf("got (%q,%q), want (\"a1,c3\",\"Alpha,Gamma\")", ids, names) } diff --git a/plugins/otel/main.go b/plugins/otel/main.go index 7837fbad13f..17cc0ed15a5 100644 --- a/plugins/otel/main.go +++ b/plugins/otel/main.go @@ -1030,18 +1030,18 @@ func buildSpanAttrs(span *schemas.Span) []attribute.KeyValue { if method == "" { method = span.Name } - teamIDs, teamNames := entitySetFromAttrs(attrs, schemas.AttrBifrostTeamIDs, schemas.AttrBifrostTeamNames, schemas.AttrTeamID, schemas.AttrTeamName) - customerIDs, customerNames := entitySetFromAttrs(attrs, schemas.AttrBifrostCustomerIDs, schemas.AttrBifrostCustomerNames, schemas.AttrCustomerID, schemas.AttrCustomerName) + teamIDs, teamNames := entitySetFromAttrs(attrs, schemas.AttrBifrostTeamIDs, schemas.AttrBifrostTeamNames, schemas.AttrBifrostTeamID, schemas.AttrBifrostTeamName) + customerIDs, customerNames := entitySetFromAttrs(attrs, schemas.AttrBifrostCustomerIDs, schemas.AttrBifrostCustomerNames, schemas.AttrBifrostCustomerID, schemas.AttrBifrostCustomerName) buIDs, buNames := entitySetFromAttrs(attrs, schemas.AttrBifrostBusinessUnitIDs, schemas.AttrBifrostBusinessUnitNames, schemas.AttrBifrostBusinessUnitID, schemas.AttrBifrostBusinessUnitName) return BuildBifrostAttributes( getStringAttr(attrs, schemas.AttrProviderName), schemas.NormalizeModelName(getStringAttr(attrs, schemas.AttrRequestModel)), method, - getStringAttr(attrs, schemas.AttrVirtualKeyID), - getStringAttr(attrs, schemas.AttrVirtualKeyName), - getStringAttr(attrs, schemas.AttrSelectedKeyID), - getStringAttr(attrs, schemas.AttrSelectedKeyName), - getIntAttr(attrs, schemas.AttrFallbackIndex), + getStringAttr(attrs, schemas.AttrBifrostVirtualKeyID), + getStringAttr(attrs, schemas.AttrBifrostVirtualKeyName), + getStringAttr(attrs, schemas.AttrBifrostSelectedKeyID), + getStringAttr(attrs, schemas.AttrBifrostSelectedKeyName), + getIntAttr(attrs, schemas.AttrBifrostFallbackIndex), teamIDs, teamNames, customerIDs, @@ -1232,22 +1232,16 @@ func (p *OtelPlugin) recordMetricsFromTrace(ctx context.Context, exporter *Metri // Record retries used for this request. Read off the final span (the last attempt's // attempt index) so the value is "total retries used", matching the Prometheus side. - retries := getIntAttr(attrs, schemas.AttrNumberOfRetries) + retries := getIntAttr(attrs, schemas.AttrBifrostRetries) exporter.RecordRequestRetries(ctx, float64(retries), otelAttrs...) - // Record token usage - try both naming conventions - inputTokens := getIntAttr(attrs, schemas.AttrPromptTokens) - if inputTokens == 0 { - inputTokens = getIntAttr(attrs, schemas.AttrInputTokens) - } + // Record token usage + inputTokens := getIntAttr(attrs, schemas.AttrInputTokens) if inputTokens > 0 { exporter.RecordInputTokens(ctx, int64(inputTokens), otelAttrs...) } - outputTokens := getIntAttr(attrs, schemas.AttrCompletionTokens) - if outputTokens == 0 { - outputTokens = getIntAttr(attrs, schemas.AttrOutputTokens) - } + outputTokens := getIntAttr(attrs, schemas.AttrOutputTokens) if outputTokens > 0 { exporter.RecordOutputTokens(ctx, int64(outputTokens), otelAttrs...) } @@ -1259,10 +1253,9 @@ func (p *OtelPlugin) recordMetricsFromTrace(ctx context.Context, exporter *Metri } // Record streaming latency metrics if available - ttft := getFloat64Attr(attrs, schemas.AttrTimeToFirstToken) + ttft := getFloat64Attr(attrs, schemas.AttrTimeToFirstChunk) if ttft > 0 { - // Convert from nanoseconds to seconds if needed (check the unit) - exporter.RecordStreamFirstTokenLatency(ctx, ttft/1e9, otelAttrs...) + exporter.RecordStreamFirstTokenLatency(ctx, ttft, otelAttrs...) } // Record provider-side prompt cache tokens (cache_read / cache_creation). Unlike the diff --git a/plugins/telemetry/main.go b/plugins/telemetry/main.go index 15552d66afe..6289012510a 100644 --- a/plugins/telemetry/main.go +++ b/plugins/telemetry/main.go @@ -753,11 +753,9 @@ func (p *PrometheusPlugin) PreLLMHook(ctx *schemas.BifrostContext, req *schemas. // applyCustomLabels resolves each configured custom label into labelValues. // Resolution order (first match wins): // 1. x-bf-dim-* headers (canonical; BifrostContextKeyDimensions) -// 2. x-bf-prom-* headers (deprecated; kept for backward compatibility) -// 3. Direct BifrostContextKey lookup (Go SDK usage — documented API) +// 2. Direct BifrostContextKey lookup (Go SDK usage — documented API) func (p *PrometheusPlugin) applyCustomLabels(ctx *schemas.BifrostContext, labelValues map[string]string) { dims, _ := ctx.Value(schemas.BifrostContextKeyDimensions).(map[string]string) - requestHeaders, _ := ctx.Value(schemas.BifrostContextKeyRequestHeaders).(map[string]string) for _, key := range p.customLabels { if dims != nil { if v, ok := dims[key]; ok { @@ -765,12 +763,6 @@ func (p *PrometheusPlugin) applyCustomLabels(ctx *schemas.BifrostContext, labelV continue } } - if requestHeaders != nil { - if v, ok := requestHeaders["x-bf-prom-"+key]; ok { - labelValues[key] = v - continue - } - } if value := ctx.Value(schemas.BifrostContextKey(key)); value != nil { if strValue, ok := value.(string); ok { labelValues[key] = strValue diff --git a/plugins/telemetry/main_test.go b/plugins/telemetry/main_test.go index 31c1f0190fa..3b9c4f9fa5e 100644 --- a/plugins/telemetry/main_test.go +++ b/plugins/telemetry/main_test.go @@ -350,3 +350,66 @@ func TestPushGatewayPushesBifrostButNotRuntimeCollectors(t *testing.T) { } func boolPtr(b bool) *bool { return &b } + +// TestApplyCustomLabels covers applyCustomLabels' resolution behavior: values +// sourced from x-bf-dim-* dimensions, values from a direct typed context key, +// and dimension precedence when both are present. Header-level exclusion of the +// removed x-bf-prom-* prefix is enforced upstream in the HTTP transport, not here. +func TestApplyCustomLabels(t *testing.T) { + tests := []struct { + name string + customLabels []string + dimensions map[string]string + typedKeys map[string]string // set via ctx.SetValue(BifrostContextKey(k), v) + want map[string]string + }{ + { + name: "resolves from dimensions", + customLabels: []string{"environment"}, + dimensions: map[string]string{"environment": "production"}, + want: map[string]string{"environment": "production"}, + }, + { + name: "resolves from direct typed context key", + customLabels: []string{"tenant"}, + typedKeys: map[string]string{"tenant": "acme"}, + want: map[string]string{"tenant": "acme"}, + }, + { + name: "dimension takes precedence over typed key", + customLabels: []string{"region"}, + dimensions: map[string]string{"region": "us-east-1"}, + typedKeys: map[string]string{"region": "eu-west-1"}, + want: map[string]string{"region": "us-east-1"}, + }, + { + name: "label absent from all sources is not emitted", + customLabels: []string{"missing"}, + want: map[string]string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := schemas.NewBifrostContext(context.Background(), time.Now().Add(time.Minute)) + if tt.dimensions != nil { + ctx.SetValue(schemas.BifrostContextKeyDimensions, tt.dimensions) + } + for k, v := range tt.typedKeys { + ctx.SetValue(schemas.BifrostContextKey(k), v) + } + + p := &PrometheusPlugin{customLabels: tt.customLabels} + got := map[string]string{} + p.applyCustomLabels(ctx, got) + + if len(got) != len(tt.want) { + t.Fatalf("label count = %d, want %d (got %v)", len(got), len(tt.want), got) + } + for k, v := range tt.want { + if got[k] != v { + t.Errorf("label %q = %q, want %q", k, got[k], v) + } + } + }) + } +} diff --git a/plugins/telemetry/utils.go b/plugins/telemetry/utils.go index 35773d816ef..6f95ab346a1 100644 --- a/plugins/telemetry/utils.go +++ b/plugins/telemetry/utils.go @@ -30,7 +30,6 @@ func getPrometheusLabelValues(expectedLabels []string, headerValues map[string]s // collectPrometheusKeyValues collects all metrics for a request including: // - Default metrics (path, method, status) // - Custom dimension headers (x-bf-dim-*) — the canonical header prefix -// - Deprecated custom prometheus headers (x-bf-prom-*) — kept for backward compatibility // Returns a map of all label values func collectPrometheusKeyValues(ctx *fasthttp.RequestCtx) map[string]string { path := string(ctx.Path()) @@ -47,24 +46,16 @@ func collectPrometheusKeyValues(ctx *fasthttp.RequestCtx) map[string]string { "method": method, } - // Collect custom dimension and prometheus headers + // Collect custom dimension headers ctx.Request.Header.All()(func(key, value []byte) bool { keyStr := strings.ToLower(string(key)) - // x-bf-dim-* (canonical; replaces x-bf-prom-*) + // x-bf-dim-* (canonical custom-dimension prefix) if labelName, ok := strings.CutPrefix(keyStr, "x-bf-dim-"); ok && labelName != "" { if labelName != "path" && labelName != "method" { // it was prepoulated in the context labelValues[labelName] = string(value) ctx.SetUserValue(keyStr, string(value)) } } - // x-bf-prom-* (deprecated; kept for backward compatibility) - if labelName, ok := strings.CutPrefix(keyStr, "x-bf-prom-"); ok && labelName != "" { - // Only set if not already provided via x-bf-dim-* (x-bf-dim takes precedence) - if _, alreadySet := labelValues[labelName]; !alreadySet && labelName != "path" && labelName != "method" { - labelValues[labelName] = string(value) - ctx.SetUserValue(keyStr, string(value)) - } - } return true }) diff --git a/transports/bifrost-http/lib/ctx.go b/transports/bifrost-http/lib/ctx.go index 007b738d44d..01e276f038f 100644 --- a/transports/bifrost-http/lib/ctx.go +++ b/transports/bifrost-http/lib/ctx.go @@ -186,8 +186,9 @@ func ResolveSessionIDFromRequest(h *fasthttp.RequestHeader) string { // - The prefix is stripped and the remainder becomes the dimension key. // - Example: 'x-bf-dim-environment' with value 'production' stores {"environment": "production"}. // -// 1a. Prometheus Headers (x-bf-prom-*) [DEPRECATED — use x-bf-dim-* instead]: -// - All headers prefixed with 'x-bf-prom-' are still accepted for backward compatibility. +// 1a. Prometheus Headers (x-bf-prom-*) [REMOVED — use x-bf-dim-* instead]: +// - No longer consumed by anything. Still swallowed here so they neither leak +// into unified dimensions nor get forwarded to the provider. // // 2. Maxim Tracing Headers (x-bf-maxim-*): // - Specifically handles 'x-bf-maxim-traceID' and 'x-bf-maxim-generationID' @@ -363,8 +364,8 @@ func ConvertToBifrostContext(ctx *fasthttp.RequestCtx, store HandlerStore) (*sch return true } if labelName, ok := strings.CutPrefix(keyStr, "x-bf-prom-"); ok && labelName != "" { - // x-bf-prom-* is Prometheus-only and must not flow into unified dimensions - // (logs/OTEL/Maxim/etc). Prometheus plugin reads headers directly. + // x-bf-prom-* is a removed legacy prefix. Swallow it so it neither flows + // into unified dimensions (logs/OTEL/Maxim/etc) nor is forwarded upstream. return true } // Checking for maxim headers