diff --git a/lib/elastic/esql.rb b/lib/elastic/esql.rb index 1b6b4ec..f0087eb 100644 --- a/lib/elastic/esql.rb +++ b/lib/elastic/esql.rb @@ -32,6 +32,7 @@ require_relative 'metadata' require_relative 'metrics_info' require_relative 'mv_expand' +require_relative 'promql' require_relative 'queryable' require_relative 'registered_domain' require_relative 'rename' @@ -54,11 +55,11 @@ module Elastic class ESQL [ ChangePoint, Custom, Dissect, Drop, Eval, Fork, Fuse, Grok, InlineStats, Keep, LookupJoin, - Metadata, MetricsInfo, MvExpand, Queryable, RegisteredDomain, Rename, Row, Sample, + Metadata, MetricsInfo, MvExpand, PromQL, Queryable, RegisteredDomain, Rename, Row, Sample, SetDirective, Show, Stats, StatsMixin, TS, URIParts, Util ].each { |m| include m } - SOURCE_COMMANDS = [:from, :row, :show, :ts].freeze + SOURCE_COMMANDS = [:from, :promql, :row, :show, :ts].freeze def initialize @query = {} @@ -107,6 +108,12 @@ def self.from(index_pattern) new.from(index_pattern) end + # Class method to allow static instantiation. + # @see PromQL#promql + def self.promql(params) + new.promql(params) + end + # The SHOW source command returns information about the deployment and its capabilities. # @return [String] 'SHOW INFO' # @see https://www.elastic.co/docs/reference/query-languages/esql/commands/source-commands#esql-show diff --git a/lib/elastic/promql.rb b/lib/elastic/promql.rb new file mode 100644 index 0000000..719e067 --- /dev/null +++ b/lib/elastic/promql.rb @@ -0,0 +1,85 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +module Elastic + # The PROMQL source command queries time series indices using Prometheus Query Language (PromQL). + # Like TS, it enables time series aggregation functions, but accepts PromQL syntax instead of + # ES|QL. + module PromQL + PROMQL_PARAMETERS = [:index, :step, :buckets, :start, :end, :scrape_interval, :result, :expression].freeze + # @param [Hash] params The PROMQL command accepts zero or more space-separated key value options + # followed by named PromQL expression. + # @option params [String] :index A list of indices, data streams, or aliases. Supports wildcards + # and date math. Defaults to * querying all indices with + # index.mode: time_series. + # @option params [String] :step Query resolution step width (optional). Automatically determined + # given the number of target +buckets+ and the selected time + # range. + # @option params [String] :buckets Target number of buckets for auto-step derivation. Defaults + # to +100+. Mutually exclusive with +step+. Requires a known + # time range, either by setting start and end explicitly or + # implicitly through Kibana's time range filter. + # @option params [String] :start Start time of the query, inclusive (optional). Uses the start + # based on Kibana's date picker or unrestricted if missing. + # @option params [String] :end End time of the query, inclusive (optional). Uses the end based + # on Kibana's date picker or unrestricted if missing. + # @option params [String] :scrape_interval The expected metric collection interval. Defaults to + # +1m+. Used to determine implicit range selector + # windows as +max(step, scrape_interval)+. + # @option params [String] :result Name of the output column with the query result timeseries + # (optional). By default, the name of the output column is the + # PromQL expression itself. + # + # @example + # Elastic::ESQL.promql(index: 'k8', step: '1h', result: 'result', expression: '(sum by (cluster) (network.cost))') + # + # @see https://www.elastic.co/docs/reference/query-languages/esql/commands/promql + # + def promql(params) + validate_params(params.keys) + @query[:promql] = promql_query(params) + self + end + + private + + def validate_params(params) + params.each do |param| + raise ArgumentError, "#{param} is not a valid parameter for PROMQL" unless PROMQL_PARAMETERS.include?(param) + end + end + + def promql_query(params) + query = [] + result = params.delete(:result) + expression = params.delete(:expression) + query += process_promql_params(params) + query << (result ? "#{result}=#{expression}" : expression) + query.join(' ') + end + + def process_promql_params(params) + params.map do |k, v| + if [:start, :end].include?(k) + "#{k}=\"#{v}\"" + else + "#{k}=#{v}" + end + end + end + end +end diff --git a/spec/promql_spec.rb b/spec/promql_spec.rb new file mode 100644 index 0000000..61ee6ab --- /dev/null +++ b/spec/promql_spec.rb @@ -0,0 +1,74 @@ +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'spec_helper' + +# rubocop:disable Metrics/BlockLength +describe Elastic::ESQL do + context 'PROMQL' do + it 'builds a query with index' do + esql = ESQL.promql(index: 'metrics-*', expression: 'sum by (instance) (rate(http_requests_total))') + expect(esql.query).to eq 'PROMQL index=metrics-* sum by (instance) (rate(http_requests_total))' + end + + it 'builds a query with more parameters' do + esql = ESQL.promql( + index: 'k8s', + step: '5m', + start: '2024-05-10T00:20:00.000Z', + end: '2024-05-10T00:25:00.000Z', + expression: '(sum(avg_over_time(network.cost[5m])))' + ) + expect(esql.query).to eq 'PROMQL index=k8s step=5m ' \ + 'start="2024-05-10T00:20:00.000Z" ' \ + 'end="2024-05-10T00:25:00.000Z" ' \ + '(sum(avg_over_time(network.cost[5m])))' + end + + it 'buils a query with named result' do + esql = ESQL.promql( + index: 'k8s', + step: '1h', + result: 'result', + expression: '(sum by (cluster) (network.cost))' + ).sort('result') + expect(esql.query).to eq 'PROMQL index=k8s step=1h ' \ + 'result=(sum by (cluster) (network.cost)) ' \ + '| SORT result' + end + + it 'buils another query from the examples' do + expression = '(max by (cluster) (network.total_bytes_in{cluster!="prod"}))' + esql = ESQL.promql( + index: 'k8s', + step: '1h', + result: 'cost', + expression: expression + ).sort('cluster') + expect(esql.query).to eq 'PROMQL index=k8s step=1h ' \ + "cost=#{expression} " \ + '| SORT cluster' + end + + it 'raises error if the parameters are wrong' do + expect do + ESQL.promql(example_parameter: 'wrong') + end.to raise_error(ArgumentError, 'example_parameter is not a valid parameter for PROMQL') + end + end +end +# rubocop:enable Metrics/BlockLength