diff --git a/ee/app/graphql/mutations/ai/action.rb b/ee/app/graphql/mutations/ai/action.rb index adcac4e09b32a91fcb9bac6b28dad9b994a0b00e..7913c076e96e671d9ac3b0c4daf0ad07528ea53e 100644 --- a/ee/app/graphql/mutations/ai/action.rb +++ b/ee/app/graphql/mutations/ai/action.rb @@ -5,6 +5,8 @@ module Ai class Action < BaseMutation graphql_name 'AiAction' + include ::Gitlab::Llm::Concerns::Logger + MUTUALLY_EXCLUSIVE_ARGUMENTS_ERROR = 'Only one method argument is required' ::Gitlab::Llm::Utils::AiFeaturesCatalogue.external.each_key do |method| @@ -47,9 +49,36 @@ def ready?(**args) super end + # rubocop:disable GraphQL/GraphqlName -- This is unrelated to GraphQL schema. + class UnsafeSanitizedPrinter < GraphQL::Language::SanitizedPrinter + def redact_argument_value?(...) + false + end + end + # rubocop:enable GraphQL/GraphqlName + + def sanitized_query_string + return unless Feature.enabled?(:expanded_ai_logging, current_user) + + # rubocop:disable GitlabSecurity/PublicSend -- Workaround for the GraphQL Ruby gem. + context.query.send(:with_prepared_ast) do + UnsafeSanitizedPrinter.new(context.query, inline_variables: true).sanitized_query_string + end + # rubocop:enable GitlabSecurity/PublicSend + end + def resolve(**attributes) verify_rate_limit! + log_conditional_info( + current_user, + message: "Received AiAction mutation GraphQL query", + event_name: 'ai_action_mutation', + ai_component: 'abstraction_layer', + user_id: current_user.id, + graphql_query: sanitized_query_string + ) + resource_id, method, options = extract_method_params!(attributes) check_feature_flag_enabled!(method) diff --git a/ee/lib/gitlab/llm/concerns/logger.rb b/ee/lib/gitlab/llm/concerns/logger.rb index c280d526ab3109ddfd460b22e61aa4275c3cb4fe..457d58ecd1a1c0fee3b17e0b7d143034302d52fe 100644 --- a/ee/lib/gitlab/llm/concerns/logger.rb +++ b/ee/lib/gitlab/llm/concerns/logger.rb @@ -46,7 +46,8 @@ module Logger Attribute.new(:event_type, String), Attribute.new(:error_type, String), Attribute.new(:fragment, String), - Attribute.new(:ai_response_server, String) + Attribute.new(:ai_response_server, String), + Attribute.new(:graphql_query, String) ].freeze def self.included(base) diff --git a/ee/spec/requests/api/graphql/mutations/projects/chat_spec.rb b/ee/spec/requests/api/graphql/mutations/projects/chat_spec.rb index f6b1f1dd003e10087245d4bf459594900d795475..1e5154f9a1a6dcc11cd40fbc42283d24676cda7c 100644 --- a/ee/spec/requests/api/graphql/mutations/projects/chat_spec.rb +++ b/ee/spec/requests/api/graphql/mutations/projects/chat_spec.rb @@ -21,6 +21,36 @@ include_context 'with ai features enabled for group' + it 'logs expanded GraphQL mutation request' do + expect_next_instance_of(Mutations::Ai::Action) do |mutation| + expect(mutation).to receive(:log_conditional_info).with( + instance_of(User), + hash_including(graphql_query: instance_of(String)) + ) + end + + post_graphql_mutation(mutation, current_user: current_user) + end + + context 'when expanded_ai_logging feature flag is disabled' do + before do + stub_feature_flags(expanded_ai_logging: false) + end + + it 'does not log expanded GraphQL mutation request' do + expect_next_instance_of(Mutations::Ai::Action) do |mutation| + expect(mutation).to receive(:log_conditional_info).with( + instance_of(User), + hash_including(graphql_query: nil) + ) + end + + expect(Mutations::Ai::Action::UnsafeSanitizedPrinter).not_to receive(:new) + + post_graphql_mutation(mutation, current_user: current_user) + end + end + context 'when resource is nil' do let(:resource) { nil }