@@ -21,15 +21,25 @@ module Integrations
2121 # Openlayer::Integrations::GoogleConversationalSearchTracer.trace_client(
2222 # google_client,
2323 # openlayer_client: openlayer,
24- # inference_pipeline_id: 'your-pipeline-id'
24+ # inference_pipeline_id: 'your-pipeline-id',
25+ # additional_columns: { environment: 'production' }
2526 # )
2627 #
27- # # Now all answer_query calls are automatically traced
28+ # # Now all answer_query calls are automatically traced! Pass
29+ # # additional_columns on an individual call to attach data (like your
30+ # # own trace ID) to just that row; it takes precedence over the
31+ # # static defaults above on a key conflict.
2832 # response = google_client.answer_query(
2933 # serving_config: "projects/.../servingConfigs/default",
30- # query: { text: "What is the meaning of life?" }
34+ # query: { text: "What is the meaning of life?" },
35+ # additional_columns: { trace_id: "abc-123" }
3136 # )
3237 class GoogleConversationalSearchTracer
38+ # Row keys computed by this tracer. Any key in a caller-supplied
39+ # additional_columns hash matching one of these is dropped, so custom
40+ # data can never overwrite core trace fields.
41+ RESERVED_ROW_KEYS = [ :query , :answer , :latency_ms , :timestamp , :metadata , :steps , :context , :session_id , :user_id ] . freeze
42+
3343 # Enable tracing on a Google ConversationalSearchService client
3444 #
3545 # @param client [Google::Cloud::DiscoveryEngine::V1::ConversationalSearchService::Client]
@@ -42,8 +52,12 @@ class GoogleConversationalSearchTracer
4252 # Optional session ID to use for all traces. Takes precedence over auto-extracted sessions.
4353 # @param user_id [String, nil]
4454 # Optional user ID to use for all traces.
55+ # @param additional_columns [Hash, nil]
56+ # Optional static column values merged into every trace sent through this client (e.g. `{ environment: 'production' }`).
57+ # A value passed to an individual answer_query call takes precedence over these on a key conflict. Keys colliding
58+ # with a reserved row column (query, answer, latency_ms, timestamp, metadata, steps, context, session_id, user_id) are dropped.
4559 # @return [void]
46- def self . trace_client ( client , openlayer_client :, inference_pipeline_id :, session_id : nil , user_id : nil )
60+ def self . trace_client ( client , openlayer_client :, inference_pipeline_id :, session_id : nil , user_id : nil , additional_columns : { } )
4761 # Store original method reference
4862 original_answer_query = client . method ( :answer_query )
4963
@@ -52,6 +66,10 @@ def self.trace_client(client, openlayer_client:, inference_pipeline_id:, session
5266 # Capture start time
5367 start_time = Time . now
5468
69+ # Extract per-call additional columns before forwarding to the
70+ # real client; Google's client never sees this key
71+ call_additional_columns = kwargs . delete ( :additional_columns )
72+
5573 # Execute the original method
5674 response = original_answer_query . call ( *args , **kwargs , &block )
5775
@@ -69,7 +87,9 @@ def self.trace_client(client, openlayer_client:, inference_pipeline_id:, session
6987 openlayer_client : openlayer_client ,
7088 inference_pipeline_id : inference_pipeline_id ,
7189 session_id : session_id ,
72- user_id : user_id
90+ user_id : user_id ,
91+ additional_columns : additional_columns ,
92+ call_additional_columns : call_additional_columns
7393 )
7494 rescue StandardError => e
7595 # Never break the user's application due to tracing errors
@@ -95,8 +115,10 @@ def self.trace_client(client, openlayer_client:, inference_pipeline_id:, session
95115 # @param inference_pipeline_id [String] Pipeline ID
96116 # @param session_id [String, nil] Optional session ID (takes precedence over auto-extracted)
97117 # @param user_id [String, nil] Optional user ID
118+ # @param additional_columns [Hash, nil] Optional static column values (see {.trace_client})
119+ # @param call_additional_columns [Hash, nil] Optional per-call column values; takes precedence over additional_columns
98120 # @return [void]
99- def self . send_trace ( args :, kwargs :, response :, start_time :, end_time :, openlayer_client :, inference_pipeline_id :, session_id : nil , user_id : nil )
121+ def self . send_trace ( args :, kwargs :, response :, start_time :, end_time :, openlayer_client :, inference_pipeline_id :, session_id : nil , user_id : nil , additional_columns : { } , call_additional_columns : { } )
100122 # Calculate latency
101123 latency_ms = ( ( end_time - start_time ) * 1000 ) . round ( 2 )
102124
@@ -199,6 +221,12 @@ def self.send_trace(args:, kwargs:, response:, start_time:, end_time:, openlayer
199221 trace_data [ :config ] [ :userIdColumnName ] = "user_id"
200222 end
201223
224+ # Merge additional columns (per-call values take precedence over
225+ # static defaults; keys colliding with reserved row columns are
226+ # dropped so custom data can never corrupt core trace fields)
227+ extra_columns = resolve_additional_columns ( additional_columns , call_additional_columns )
228+ trace_data [ :rows ] [ 0 ] . merge! ( extra_columns ) unless extra_columns . empty?
229+
202230 # Send to Openlayer
203231 openlayer_client
204232 . inference_pipelines
@@ -594,6 +622,45 @@ def self.extract_query_understanding_info(answer)
594622 nil
595623 end
596624
625+ # Merge static and per-call additional columns into a single Hash of
626+ # extra row columns. Call-level values take precedence over static
627+ # ones on key conflict, and any key colliding with a reserved row
628+ # column is dropped.
629+ #
630+ # @param static_columns [Object] Value passed to trace_client (expected Hash)
631+ # @param call_columns [Object] Value passed to an individual answer_query call (expected Hash)
632+ # @return [Hash] Extra columns safe to merge onto a trace row
633+ def self . resolve_additional_columns ( static_columns , call_columns )
634+ merged = normalize_additional_columns ( static_columns ) . merge ( normalize_additional_columns ( call_columns ) )
635+
636+ merged . each_with_object ( { } ) do |( key , value ) , result |
637+ if RESERVED_ROW_KEYS . include? ( key )
638+ warn_if_debug ( "[Openlayer] additional_columns key :#{ key } collides with a reserved column and was ignored" )
639+ else
640+ result [ key ] = value
641+ end
642+ end
643+ end
644+
645+ # Normalize an additional_columns value into a Hash with Symbol keys.
646+ # Non-Hash input (or a key that can't be a Symbol) is dropped rather
647+ # than raising, so a caller mistake can never break tracing.
648+ #
649+ # @param columns [Object] Expected to be a Hash of column name => value
650+ # @return [Hash]
651+ def self . normalize_additional_columns ( columns )
652+ return { } unless columns . is_a? ( Hash )
653+
654+ columns . each_with_object ( { } ) do |( key , value ) , result |
655+ next unless key . respond_to? ( :to_sym )
656+
657+ result [ key . to_sym ] = value
658+ end
659+ rescue StandardError => e
660+ warn_if_debug ( "[Openlayer] Failed to normalize additional columns: #{ e . message } " )
661+ { }
662+ end
663+
597664 # Safely extract a field from an object
598665 #
599666 # @param obj [Object] Object to extract from
@@ -659,6 +726,8 @@ def self.warn_if_debug(message)
659726 :extract_session ,
660727 :extract_user_pseudo_id ,
661728 :extract_query_understanding_info ,
729+ :resolve_additional_columns ,
730+ :normalize_additional_columns ,
662731 :safe_extract ,
663732 :safe_count ,
664733 :extract_timestamp
0 commit comments