1# frozen_string_literal: true 2 3module Prometheus 4 class ProxyVariableSubstitutionService < BaseService 5 include Stepable 6 7 VARIABLE_INTERPOLATION_REGEX = / 8 {{ # Variable needs to be wrapped in these chars. 9 \s* # Allow whitespace before and after the variable name. 10 (?<variable> # Named capture. 11 \w+ # Match one or more word characters. 12 ) 13 \s* 14 }} 15 /x.freeze 16 17 steps :validate_variables, 18 :add_params_to_result, 19 :substitute_params, 20 :substitute_variables 21 22 # @param environment [Environment] 23 # @param params [Hash<Symbol,Any>] 24 # @param params - query [String] The Prometheus query string. 25 # @param params - start [String] (optional) A time string in the rfc3339 format. 26 # @param params - start_time [String] (optional) A time string in the rfc3339 format. 27 # @param params - end [String] (optional) A time string in the rfc3339 format. 28 # @param params - end_time [String] (optional) A time string in the rfc3339 format. 29 # @param params - variables [ActionController::Parameters] (optional) Variables with their values. 30 # The keys in the Hash should be the name of the variable. The value should be the value of the 31 # variable. Ex: `ActionController::Parameters.new(variable1: 'value 1', variable2: 'value 2').permit!` 32 # @return [Prometheus::ProxyVariableSubstitutionService] 33 # 34 # Example: 35 # Prometheus::ProxyVariableSubstitutionService.new(environment, { 36 # params: { 37 # start_time: '2020-07-03T06:08:36Z', 38 # end_time: '2020-07-03T14:08:52Z', 39 # query: 'up{instance="{{instance}}"}', 40 # variables: { instance: 'srv1' } 41 # } 42 # }) 43 def initialize(environment, params = {}) 44 @environment = environment 45 @params = params.deep_dup 46 end 47 48 # @return - params [Hash<Symbol,Any>] Returns a Hash containing a params key which is 49 # similar to the `params` that is passed to the initialize method with 2 differences: 50 # 1. Variables in the query string are substituted with their values. 51 # If a variable present in the query string has no known value (values 52 # are obtained from the `variables` Hash in `params` or from 53 # `Gitlab::Prometheus::QueryVariables.call`), it will not be substituted. 54 # 2. `start` and `end` keys are added, with their values copied from `start_time` 55 # and `end_time`. 56 # 57 # Example output: 58 # 59 # { 60 # params: { 61 # start_time: '2020-07-03T06:08:36Z', 62 # start: '2020-07-03T06:08:36Z', 63 # end_time: '2020-07-03T14:08:52Z', 64 # end: '2020-07-03T14:08:52Z', 65 # query: 'up{instance="srv1"}', 66 # variables: { instance: 'srv1' } 67 # } 68 # } 69 def execute 70 execute_steps 71 end 72 73 private 74 75 def validate_variables(_result) 76 return success unless variables 77 78 unless variables.is_a?(ActionController::Parameters) 79 return error(_('Optional parameter "variables" must be a Hash. Ex: variables[key1]=value1')) 80 end 81 82 success 83 end 84 85 def add_params_to_result(result) 86 result[:params] = params 87 88 success(result) 89 end 90 91 def substitute_params(result) 92 start_time = result[:params][:start_time] 93 end_time = result[:params][:end_time] 94 95 result[:params][:start] = start_time if start_time 96 result[:params][:end] = end_time if end_time 97 98 success(result) 99 end 100 101 def substitute_variables(result) 102 return success(result) unless query(result) 103 104 result[:params][:query] = gsub(query(result), full_context(result)) 105 106 success(result) 107 end 108 109 def gsub(string, context) 110 # Search for variables of the form `{{variable}}` in the string and replace 111 # them with their value. 112 string.gsub(VARIABLE_INTERPOLATION_REGEX) do |match| 113 # Replace with the value of the variable, or if there is no such variable, 114 # replace the invalid variable with itself. So, 115 # `up{instance="{{invalid_variable}}"}` will remain 116 # `up{instance="{{invalid_variable}}"}` after substitution. 117 context.fetch($~[:variable], match) 118 end 119 end 120 121 def predefined_context(result) 122 Gitlab::Prometheus::QueryVariables.call( 123 @environment, 124 start_time: start_timestamp(result), 125 end_time: end_timestamp(result) 126 ).stringify_keys 127 end 128 129 def full_context(result) 130 @full_context ||= predefined_context(result).reverse_merge(variables_hash) 131 end 132 133 def variables 134 params[:variables] 135 end 136 137 def variables_hash 138 variables.to_h 139 end 140 141 def start_timestamp(result) 142 Time.rfc3339(result[:params][:start]) 143 rescue ArgumentError 144 end 145 146 def end_timestamp(result) 147 Time.rfc3339(result[:params][:end]) 148 rescue ArgumentError 149 end 150 151 def query(result) 152 result[:params][:query] 153 end 154 end 155end 156