UNPKG

@dash0hq/codemirror-promql

Version:
717 lines 41.4 kB
// Copyright 2021 The Prometheus Authors // Licensed 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. import { Add, AggregateExpr, And, BinaryExpr, BoolModifier, Div, Eql, EqlRegex, EqlSingle, FunctionCallBody, GroupingLabels, Gte, Gtr, LabelMatchers, LabelName, Lss, Lte, MatchOp, MatrixSelector, Identifier, Mod, Mul, Neq, NeqRegex, OffsetExpr, Or, Pow, PromQL, StepInvariantExpr, StringLiteral, Sub, SubqueryExpr, Unless, VectorSelector, UnquotedLabelMatcher, QuotedLabelMatcher, QuotedLabelName, NumberDurationLiteralInDurationContext, NumberDurationLiteral, AggregateOp, Topk, Bottomk, LimitK, LimitRatio, CountValues, } from '@prometheus-io/lezer-promql'; import { buildLabelMatchers, containsAtLeastOneChild, containsChild, walkBackward } from '../parser'; import { aggregateOpModifierTerms, aggregateOpTerms, atModifierTerms, binOpModifierTerms, binOpTerms, durationTerms, functionIdentifierTerms, matchOpTerms, numberTerms, snippets, } from './promql.terms'; import { Matcher } from '../types'; import { syntaxTree } from '@codemirror/language'; // Default list of label names that define a metric name in PromQL matchers. // This can be customized to support additional metric-defining labels. export const METRIC_NAME_LABELS = ['__name__', 'otel_metric_name']; const autocompleteNodes = { matchOp: matchOpTerms, binOp: binOpTerms, duration: durationTerms, binOpModifier: binOpModifierTerms, atModifier: atModifierTerms, functionIdentifier: functionIdentifierTerms, aggregateOp: aggregateOpTerms, aggregateOpModifier: aggregateOpModifierTerms, number: numberTerms, }; // ContextKind is the different possible value determinate by the autocompletion export var ContextKind; (function (ContextKind) { // dynamic autocompletion (required a distant server) ContextKind[ContextKind["MetricName"] = 0] = "MetricName"; ContextKind[ContextKind["LabelName"] = 1] = "LabelName"; ContextKind[ContextKind["LabelValue"] = 2] = "LabelValue"; // static autocompletion ContextKind[ContextKind["Function"] = 3] = "Function"; ContextKind[ContextKind["Aggregation"] = 4] = "Aggregation"; ContextKind[ContextKind["BinOpModifier"] = 5] = "BinOpModifier"; ContextKind[ContextKind["BinOp"] = 6] = "BinOp"; ContextKind[ContextKind["MatchOp"] = 7] = "MatchOp"; ContextKind[ContextKind["AggregateOpModifier"] = 8] = "AggregateOpModifier"; ContextKind[ContextKind["Duration"] = 9] = "Duration"; ContextKind[ContextKind["Offset"] = 10] = "Offset"; ContextKind[ContextKind["Bool"] = 11] = "Bool"; ContextKind[ContextKind["AtModifiers"] = 12] = "AtModifiers"; ContextKind[ContextKind["Number"] = 13] = "Number"; })(ContextKind || (ContextKind = {})); function getMetricNameInGroupBy(tree, state) { // There should be an AggregateExpr as parent of the GroupingLabels. // Then we should find the VectorSelector child to be able to find the metric name. const currentNode = walkBackward(tree, AggregateExpr); if (!currentNode) { return { metricName: '', definingMatchers: null }; } let metricResult = { metricName: '', definingMatchers: null }; currentNode.cursor().iterate((node) => { // Continue until we find the VectorSelector, then look up the metric name. if (node.type.id === VectorSelector) { metricResult = getMetricNameInVectorSelector(node.node, state); if (metricResult.metricName) { return false; } } }); return metricResult; } export function getMetricNameInVectorSelector(tree, state) { // Find if there is a defined metric name. Should be used to autocomplete a labelValue or a labelName // First find the parent "VectorSelector" to be able to find then the subChild "Identifier" if it exists. let currentNode = walkBackward(tree, VectorSelector); if (!currentNode) { // Weird case that shouldn't happen, because "VectorSelector" is by definition the parent of the LabelMatchers. return { metricName: '', definingMatchers: null }; } const identifier = currentNode.getChild(Identifier); if (identifier) { return { metricName: state.sliceDoc(identifier.from, identifier.to), definingMatchers: null }; } const labelMatchers = currentNode.getChild(LabelMatchers); if (labelMatchers) { return findMetricNameInLabelMatchers(labelMatchers, state); } return { metricName: '', definingMatchers: null }; } /** * Attempts to extract the metric name from a label matcher block such as: * `{__name__="http_requests_total"}`. * * This is a fallback strategy when the metric name is not provided as an identifier * but instead is stored as a label matcher using one of the metric-defining labels * (e.g., `__name__` or custom labels defined in `METRIC_NAME_LABELS`) * * It only considers exact match expressions like `__name__="metric"` and ignores * regex matches (`=~`, `!~`) or inequality operators (`!=`, `<`, etc). * * Useful when autocompleting or analyzing metric selectors in PromQL syntax trees. */ export function findMetricNameInLabelMatchers(labelMatchers, state) { var _a; // Validate that we are working with a LabelMatchers node. // If not, return early with no result. if (!labelMatchers || labelMatchers.type.id !== LabelMatchers) { return { metricName: '', definingMatchers: null }; } // Initialize a cursor to iterate through the label matchers inside the `{...}` block. let cursor = labelMatchers.cursor(); // Move the cursor to the first child (first individual matcher) if it exists. if (cursor.firstChild()) { do { // Try to find the LabelName (e.g., "__name__"), the StringLiteral (e.g., '"metric"'), // and the exact match operator (EqlSingle, representing `=`). const labelNameNode = cursor.node.getChild(LabelName); const valueNode = cursor.node.getChild(StringLiteral); const operatorNode = (_a = cursor.node.getChild(MatchOp)) === null || _a === void 0 ? void 0 : _a.getChild(EqlSingle); // ensures it's an `=` match // Continue only if all three parts are present — this ensures we ignore things like `=~`, `!=`, etc. if (labelNameNode && valueNode && operatorNode) { // Extract the actual label name from the source text. const labelName = state.sliceDoc(labelNameNode.from, labelNameNode.to); // Check if the label name is one of the expected metric name indicators (default: "__name__"). if (METRIC_NAME_LABELS.includes(labelName)) { // Extract the raw value string, which is quoted. const raw = state.sliceDoc(valueNode.from, valueNode.to); // Remove surrounding quotes from the string literal to get the plain metric name. const metricName = raw.replace(/^"(.*)"$/, '$1'); if (labelName !== '__name__') { const definingMatcher = new Matcher(EqlSingle, labelName, metricName); return { metricName: '', definingMatchers: definingMatcher }; } return { metricName: metricName, definingMatchers: null }; } } // Move to the next sibling matcher inside the `{...}` block. } while (cursor.nextSibling()); } // If no matching label matcher is found, return an empty string. return { metricName: '', definingMatchers: null }; } function arrayToCompletionResult(data, from, to, includeSnippet = false, span = true) { const options = data; if (includeSnippet) { options.push(...snippets); } return { from: from, to: to, options: options, validFor: span ? /^[a-zA-Z0-9_:]+$/ : undefined, }; } // computeStartCompleteLabelPositionInLabelMatcherOrInGroupingLabel calculates the start position only when the node is a LabelMatchers or a GroupingLabels function computeStartCompleteLabelPositionInLabelMatcherOrInGroupingLabel(node, pos) { // Here we can have two different situations: // 1. `metric{}` or `sum by()` with the cursor between the bracket // and so we have increment the starting position to avoid to consider the open bracket when filtering the autocompletion list. // 2. `metric{foo="bar",} or `sum by(foo,) with the cursor after the comma. // Then the start number should be the current position to avoid to consider the previous labelMatcher/groupingLabel when filtering the autocompletion list. let start = node.from + 1; if (node.firstChild !== null) { // here that means the LabelMatchers / GroupingLabels has a child, which is not possible if we have the expression `metric{}`. So we are likely trying to autocomplete the label list after a comma start = pos; } return start; } // computeStartCompletePosition calculates the start position of the autocompletion. // It is an important step because the start position will be used by CMN to find the string and then to use it to filter the CompletionResult. // A wrong `start` position will lead to have the completion not working. // Note: this method is exported only for testing purpose. export function computeStartCompletePosition(state, node, pos) { var _a, _b, _c, _d, _e, _f, _g; const currentText = state.doc.slice(node.from, pos).toString(); let start = node.from; if (node.type.id === LabelMatchers || node.type.id === GroupingLabels) { start = computeStartCompleteLabelPositionInLabelMatcherOrInGroupingLabel(node, pos); } else if ((node.type.id === FunctionCallBody && node.firstChild === null) || (node.type.id === StringLiteral && (((_a = node.parent) === null || _a === void 0 ? void 0 : _a.type.id) === UnquotedLabelMatcher || ((_b = node.parent) === null || _b === void 0 ? void 0 : _b.type.id) === QuotedLabelMatcher))) { // When the cursor is between bracket, quote, we need to increment the starting position to avoid to consider the open bracket/ first string. start++; } else if (node.type.id === OffsetExpr || // Since duration and number are equivalent, writing go[5] or go[5d] is syntactically accurate. // Before we were able to guess when we had to autocomplete the duration later based on the error node, // which is not possible anymore. // So we have to analyze the string about the current node to see if the duration unit is already present or not. (node.type.id === NumberDurationLiteralInDurationContext && !durationTerms.map((v) => v.label).includes(currentText[currentText.length - 1])) || (node.type.id === NumberDurationLiteral && ((_c = node.parent) === null || _c === void 0 ? void 0 : _c.type.id) === 0 && ((_d = node.parent.parent) === null || _d === void 0 ? void 0 : _d.type.id) === SubqueryExpr) || (node.type.id === FunctionCallBody && isAggregatorWithParam(node) && node.firstChild !== null) || (node.type.id === 0 && (((_e = node.parent) === null || _e === void 0 ? void 0 : _e.type.id) === OffsetExpr || ((_f = node.parent) === null || _f === void 0 ? void 0 : _f.type.id) === MatrixSelector || (((_g = node.parent) === null || _g === void 0 ? void 0 : _g.type.id) === SubqueryExpr && containsAtLeastOneChild(node.parent, NumberDurationLiteralInDurationContext))))) { start = pos; } return start; } function isAggregatorWithParam(functionCallBody) { var _a; const parent = functionCallBody.parent; if (parent !== null && ((_a = parent.firstChild) === null || _a === void 0 ? void 0 : _a.type.id) === AggregateOp) { const aggregationOpType = parent.firstChild.firstChild; if (aggregationOpType !== null && [Topk, Bottomk, LimitK, LimitRatio, CountValues].includes(aggregationOpType.type.id)) { return true; } } return false; } // analyzeCompletion is going to determinate what should be autocompleted. // The value of the autocompletion is then calculate by the function buildCompletion. // Note: this method is exported for testing purpose only. Do not use it directly. export function analyzeCompletion(state, node, pos) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w; const result = []; switch (node.type.id) { case 0: { // 0 is the id of the error node if (((_a = node.parent) === null || _a === void 0 ? void 0 : _a.type.id) === OffsetExpr) { // we are likely in the given situation: // `metric_name offset 5` that leads to this tree: // `OffsetExpr(VectorSelector(Identifier),Offset,⚠)` // Here we can just autocomplete a duration. result.push({ kind: ContextKind.Duration }); break; } if (((_b = node.parent) === null || _b === void 0 ? void 0 : _b.type.id) === UnquotedLabelMatcher || ((_c = node.parent) === null || _c === void 0 ? void 0 : _c.type.id) === QuotedLabelMatcher) { // In this case the current token is not itself a valid match op yet: // metric_name{labelName!} result.push({ kind: ContextKind.MatchOp }); break; } if (((_d = node.parent) === null || _d === void 0 ? void 0 : _d.type.id) === MatrixSelector) { // we are likely in the given situation: // `metric_name{}[5]` // We can also just autocomplete a duration result.push({ kind: ContextKind.Duration }); break; } if (((_e = node.parent) === null || _e === void 0 ? void 0 : _e.type.id) === SubqueryExpr && containsAtLeastOneChild(node.parent, NumberDurationLiteralInDurationContext)) { // we are likely in the given situation: // `rate(foo[5d:5])` // so we should autocomplete a duration result.push({ kind: ContextKind.Duration }); break; } // when we are in the situation 'metric_name !', we have the following tree // VectorSelector(Identifier,⚠) // We should try to know if the char '!' is part of a binOp. // Note: as it is quite experimental, maybe it requires more condition and to check the current tree (parent, other child at the same level ..etc.). const operator = state.sliceDoc(node.from, node.to); if (binOpTerms.filter((term) => term.label.includes(operator)).length > 0) { result.push({ kind: ContextKind.BinOp }); } break; } case Identifier: { // sometimes an Identifier has an error has parent. This should be treated in priority if (((_f = node.parent) === null || _f === void 0 ? void 0 : _f.type.id) === 0) { const errorNodeParent = node.parent.parent; if ((errorNodeParent === null || errorNodeParent === void 0 ? void 0 : errorNodeParent.type.id) === StepInvariantExpr) { // we are likely in the given situation: // `expr @ s` // we can autocomplete start / end result.push({ kind: ContextKind.AtModifiers }); break; } if ((errorNodeParent === null || errorNodeParent === void 0 ? void 0 : errorNodeParent.type.id) === AggregateExpr) { // it matches 'sum() b'. So here we can autocomplete: // - the aggregate operation modifier // - the binary operation (since it's not mandatory to have an aggregate operation modifier) result.push({ kind: ContextKind.AggregateOpModifier }, { kind: ContextKind.BinOp }); break; } if ((errorNodeParent === null || errorNodeParent === void 0 ? void 0 : errorNodeParent.type.id) === VectorSelector) { // it matches 'sum b'. So here we also have to autocomplete the aggregate operation modifier only // if the associated identifier is matching an aggregation operation. // Note: here is the corresponding tree in order to understand the situation: // VectorSelector( // Identifier, // ⚠(Identifier) // ) const { metricName } = getMetricNameInVectorSelector(node, state); if (aggregateOpTerms.filter((term) => term.label === metricName).length > 0) { result.push({ kind: ContextKind.AggregateOpModifier }); } // It's possible it also match the expr 'metric_name unle'. // It's also possible that the operator is also a metric even if it matches the list of aggregation function. // So we also have to autocomplete the binary operator. // // The expr `metric_name off` leads to the same tree. So we have to provide the offset keyword too here. result.push({ kind: ContextKind.BinOp }, { kind: ContextKind.Offset }); break; } if (errorNodeParent && containsChild(errorNodeParent, 'Expr')) { // this last case can appear with the following expression: // 1. http_requests_total{method="GET"} off // 2. rate(foo[5m]) un // 3. sum(http_requests_total{method="GET"} off) // For these different cases we have this kind of tree: // Parent ( // ⚠(Identifier) // ) // We don't really care about the parent, here we are more interested if in the siblings of the error node, there is the node 'Expr' // If it is the case, then likely we should autocomplete the BinOp or the offset. result.push({ kind: ContextKind.BinOp }, { kind: ContextKind.Offset }); break; } } // As the leaf Identifier is coming for different cases, we have to take a bit time to analyze the tree // in order to know what we have to autocomplete exactly. // Here is some cases: // 1. metric_name / ignor --> we should autocomplete the BinOpModifier + metric/function/aggregation // 2. sum(http_requests_total{method="GET"} / o) --> BinOpModifier + metric/function/aggregation // Examples above give a different tree each time and ends up to be treated in this case. // But they all have the following common tree pattern: // Parent( ..., // ... , // VectorSelector(Identifier) // ) // // So the first things to do is to get the `Parent` and to determinate if we are in this configuration. // Otherwise we would just have to autocomplete the metric / function / aggregation. const parent = (_g = node.parent) === null || _g === void 0 ? void 0 : _g.parent; if (!parent) { // this case can be possible if the topNode is not anymore PromQL but MetricName. // In this particular case, then we just want to autocomplete the metric result.push({ kind: ContextKind.MetricName, metricName: state.sliceDoc(node.from, node.to) }); break; } // now we have to know if we have two Expr in the direct children of the `parent` const containExprTwice = containsChild(parent, 'Expr', 'Expr'); if (containExprTwice && parent.type.id !== FunctionCallBody) { if (parent.type.id === BinaryExpr && !containsAtLeastOneChild(parent, 0)) { // We are likely in the case 1 or 5 result.push({ kind: ContextKind.MetricName, metricName: state.sliceDoc(node.from, node.to) }, { kind: ContextKind.Function }, { kind: ContextKind.Aggregation }, { kind: ContextKind.BinOpModifier }, { kind: ContextKind.Number }); // in case the BinaryExpr is a comparison, we should autocomplete the `bool` keyword. But only if it is not present. // When the `bool` keyword is NOT present, then the expression looks like this: // BinaryExpr( ..., Gtr , ... ) // When the `bool` keyword is present, then the expression looks like this: // BinaryExpr( ..., Gtr , BoolModifier(...), ... ) if (containsAtLeastOneChild(parent, Eql, Gte, Gtr, Lte, Lss, Neq) && !containsAtLeastOneChild(parent, BoolModifier)) { result.push({ kind: ContextKind.Bool }); } } } else { result.push({ kind: ContextKind.MetricName, metricName: state.sliceDoc(node.from, node.to) }, { kind: ContextKind.Function }, { kind: ContextKind.Aggregation }); if (parent.type.id !== FunctionCallBody && parent.type.id !== MatrixSelector) { // it's too avoid to autocomplete a number in situation where it shouldn't. // Like with `sum by(rat)` result.push({ kind: ContextKind.Number }); } } break; } case PromQL: if (node.firstChild !== null && node.firstChild.type.id === 0) { // this situation can happen when there is nothing in the text area and the user is explicitly triggering the autocompletion (with ctrl + space) result.push({ kind: ContextKind.MetricName, metricName: '' }, { kind: ContextKind.Function }, { kind: ContextKind.Aggregation }, { kind: ContextKind.Number }); } break; case GroupingLabels: // In this case we are in the given situation: // sum by () or sum (metric_name) by () // so we have or to autocomplete any kind of labelName or to autocomplete only the labelName associated to the metric const { metricName: groupByMetricName, definingMatchers: groupByMatchers } = getMetricNameInGroupBy(node, state); result.push({ kind: ContextKind.LabelName, metricName: groupByMetricName, matchers: groupByMatchers ? [groupByMatchers] : undefined }); break; case LabelMatchers: // In that case we are in the given situation: // metric_name{} or {} // so we have or to autocomplete any kind of labelName or to autocomplete only the labelName associated to the metric const { metricName, definingMatchers } = getMetricNameInVectorSelector(node, state); result.push({ kind: ContextKind.LabelName, metricName: metricName, matchers: definingMatchers ? [definingMatchers] : undefined }); break; case LabelName: if (((_h = node.parent) === null || _h === void 0 ? void 0 : _h.type.id) === GroupingLabels) { // In this case we are in the given situation: // sum by (myL) // So we have to continue to autocomplete any kind of labelName result.push({ kind: ContextKind.LabelName }); } else if (((_j = node.parent) === null || _j === void 0 ? void 0 : _j.type.id) === UnquotedLabelMatcher) { // In that case we are in the given situation: // metric_name{myL} or {myL} // so we have or to continue to autocomplete any kind of labelName or // to continue to autocomplete only the labelName associated to the metric const { metricName, definingMatchers } = getMetricNameInVectorSelector(node, state); result.push({ kind: ContextKind.LabelName, metricName: metricName, matchers: definingMatchers ? [definingMatchers] : undefined, }); } break; case StringLiteral: if (((_k = node.parent) === null || _k === void 0 ? void 0 : _k.type.id) === UnquotedLabelMatcher || ((_l = node.parent) === null || _l === void 0 ? void 0 : _l.type.id) === QuotedLabelMatcher) { // In this case we are in the given situation: // metric_name{labelName=""} or metric_name{"labelName"=""} // So we can autocomplete the labelValue // Get the labelName. // By definition it's the firstChild: https://github.com/promlabs/lezer-promql/blob/0ef65e196a8db6a989ff3877d57fd0447d70e971/src/promql.grammar#L250 let labelName = ''; if (((_m = node.parent.firstChild) === null || _m === void 0 ? void 0 : _m.type.id) === LabelName) { labelName = state.sliceDoc(node.parent.firstChild.from, node.parent.firstChild.to); } else if (((_o = node.parent.firstChild) === null || _o === void 0 ? void 0 : _o.type.id) === QuotedLabelName) { labelName = state.sliceDoc(node.parent.firstChild.from, node.parent.firstChild.to).slice(1, -1); } //Identify the current matcher being edited const currentMatcher = node.parent; // Collect all other matcher in the LabelMatchers node, excluding the current matcher const matcherNode = walkBackward(node, LabelMatchers); const labelMatcherOpts = [QuotedLabelMatcher, UnquotedLabelMatcher]; let labelMatchers = []; if (matcherNode) { let allMatchers = []; for (const labelMatcherOpt of labelMatcherOpts) { allMatchers = allMatchers.concat(matcherNode.getChildren(labelMatcherOpt)); } //Exclude the current matcher to avoid including its incomplete value const otherMatchers = allMatchers.filter((m) => m !== currentMatcher); //Convert the remaining matchers to Matcher Objects labelMatchers = buildLabelMatchers(otherMatchers, state); } //Set the metric name, handling the special case for __name__ let resolvedMetricName = ''; if (!METRIC_NAME_LABELS.includes(labelName)) { const { metricName } = getMetricNameInVectorSelector(node, state); resolvedMetricName = metricName; } //Add the autocompletion context for label values result.push({ kind: ContextKind.LabelValue, metricName: resolvedMetricName, labelName: labelName, matchers: labelMatchers, }); } else if (((_q = (_p = node.parent) === null || _p === void 0 ? void 0 : _p.parent) === null || _q === void 0 ? void 0 : _q.type.id) === GroupingLabels) { // In this case we are in the given situation: // sum by ("myL") // So we have to continue to autocomplete any kind of labelName result.push({ kind: ContextKind.LabelName }); } else if (((_s = (_r = node.parent) === null || _r === void 0 ? void 0 : _r.parent) === null || _s === void 0 ? void 0 : _s.type.id) === LabelMatchers) { // In that case we are in the given situation: // {""} or {"metric_"} // since this is for the QuotedMetricName we need to continue to autocomplete for the metric names result.push({ kind: ContextKind.MetricName, metricName: state.sliceDoc(node.from, node.to).slice(1, -1) }); } break; case NumberDurationLiteral: if (((_t = node.parent) === null || _t === void 0 ? void 0 : _t.type.id) === 0 && ((_u = node.parent.parent) === null || _u === void 0 ? void 0 : _u.type.id) === SubqueryExpr) { // Here we are likely in this situation: // `go[5d:4]` // and we have the given tree: // SubqueryExpr( // VectorSelector(Identifier), // Duration, Duration, ⚠(NumberLiteral) // ) // So we should continue to autocomplete a duration result.push({ kind: ContextKind.Duration }); } else { result.push({ kind: ContextKind.Number }); } break; case NumberDurationLiteralInDurationContext: case OffsetExpr: result.push({ kind: ContextKind.Duration }); break; case FunctionCallBody: // For aggregation function such as Topk, the first parameter is a number. // The second one is an expression. // When moving to the second parameter, the node is an error node. // Unfortunately, as a current node, codemirror doesn't give us the error node but instead the FunctionCallBody // The tree looks like that: PromQL(AggregateExpr(AggregateOp(Topk),FunctionCallBody(NumberDurationLiteral,⚠))) // So, we need to figure out if the cursor is on the first parameter or in the second. if (isAggregatorWithParam(node)) { if (node.firstChild === null || (node.firstChild.from <= pos && node.firstChild.to >= pos)) { // it means the FunctionCallBody has no child, which means we are autocompleting the first parameter result.push({ kind: ContextKind.Number }); break; } // at this point we are necessary autocompleting the second parameter result.push({ kind: ContextKind.MetricName, metricName: '' }, { kind: ContextKind.Function }, { kind: ContextKind.Aggregation }); break; } // In all other cases, we are in the given situation: // sum() or in rate() // with the cursor between the bracket. So we can autocomplete the metric, the function and the aggregation. result.push({ kind: ContextKind.MetricName, metricName: '' }, { kind: ContextKind.Function }, { kind: ContextKind.Aggregation }); break; case Neq: if (((_v = node.parent) === null || _v === void 0 ? void 0 : _v.type.id) === MatchOp) { result.push({ kind: ContextKind.MatchOp }); } else if (((_w = node.parent) === null || _w === void 0 ? void 0 : _w.type.id) === BinaryExpr) { result.push({ kind: ContextKind.BinOp }); } break; case EqlSingle: case EqlRegex: case NeqRegex: case MatchOp: result.push({ kind: ContextKind.MatchOp }); break; case Pow: case Mul: case Div: case Mod: case Add: case Sub: case Eql: case Gte: case Gtr: case Lte: case Lss: case And: case Unless: case Or: case BinaryExpr: result.push({ kind: ContextKind.BinOp }); break; } return result; } // HybridComplete provides a full completion result with or without a remote prometheus. export class HybridComplete { constructor(prometheusClient, maxMetricsMetadata = 10000) { this.prometheusClient = prometheusClient; this.maxMetricsMetadata = maxMetricsMetadata; } getPrometheusClient() { return this.prometheusClient; } promQL(context) { const { state, pos } = context; const tree = syntaxTree(state).resolve(pos, -1); // The lines above can help you to print the current lezer tree. // It's useful when you are trying to understand why it doesn't autocomplete. // console.log(syntaxTree(state).topNode.toString()); // console.log(`current node: ${tree.type.name}`); const contexts = analyzeCompletion(state, tree, pos); let asyncResult = Promise.resolve([]); let completeSnippet = false; let span = true; for (const context of contexts) { switch (context.kind) { case ContextKind.Aggregation: completeSnippet = true; asyncResult = asyncResult.then((result) => { return result.concat(autocompleteNodes.aggregateOp); }); break; case ContextKind.Function: completeSnippet = true; asyncResult = asyncResult.then((result) => { return result.concat(autocompleteNodes.functionIdentifier); }); break; case ContextKind.BinOpModifier: asyncResult = asyncResult.then((result) => { return result.concat(autocompleteNodes.binOpModifier); }); break; case ContextKind.BinOp: asyncResult = asyncResult.then((result) => { return result.concat(autocompleteNodes.binOp); }); break; case ContextKind.MatchOp: asyncResult = asyncResult.then((result) => { return result.concat(autocompleteNodes.matchOp); }); break; case ContextKind.AggregateOpModifier: asyncResult = asyncResult.then((result) => { return result.concat(autocompleteNodes.aggregateOpModifier); }); break; case ContextKind.Duration: span = false; asyncResult = asyncResult.then((result) => { return result.concat(autocompleteNodes.duration); }); break; case ContextKind.Offset: asyncResult = asyncResult.then((result) => { return result.concat([{ label: 'offset' }]); }); break; case ContextKind.Bool: asyncResult = asyncResult.then((result) => { return result.concat([{ label: 'bool' }]); }); break; case ContextKind.AtModifiers: asyncResult = asyncResult.then((result) => { return result.concat(autocompleteNodes.atModifier); }); break; case ContextKind.Number: asyncResult = asyncResult.then((result) => { return result.concat(autocompleteNodes.number); }); break; case ContextKind.MetricName: asyncResult = asyncResult.then((result) => { return this.autocompleteMetricName(result, context); }); break; case ContextKind.LabelName: asyncResult = asyncResult.then((result) => { return this.autocompleteLabelName(result, context); }); break; case ContextKind.LabelValue: asyncResult = asyncResult.then((result) => { return this.autocompleteLabelValue(result, context); }); } } return asyncResult.then((result) => { return arrayToCompletionResult(result, computeStartCompletePosition(state, tree, pos), pos, completeSnippet, span); }); } autocompleteMetricName(result, context) { if (!this.prometheusClient) { return result; } const metricCompletion = new Map(); return this.prometheusClient .metricNames(context.metricName) .then((metricNames) => { var _a; for (const metricName of metricNames) { metricCompletion.set(metricName, { label: metricName, type: 'none' }); } // avoid to get all metric metadata if the prometheus server is too big if (metricNames.length <= this.maxMetricsMetadata) { // in order to enrich the completion list of the metric, // we are trying to find the associated metadata return (_a = this.prometheusClient) === null || _a === void 0 ? void 0 : _a.metricMetadata(); } }) .then((metricMetadata) => { if (metricMetadata) { for (const [metricName, node] of metricCompletion) { // For histograms and summaries, the metadata is only exposed for the base metric name, // not separately for the _count, _sum, and _bucket time series. const metadata = metricMetadata[metricName.replace(/(_count|_sum|_bucket)$/, '')]; if (metadata) { if (metadata.length > 1) { // it means the metricName has different possible helper and type for (const m of metadata) { if (node.detail === '') { node.detail = m.type; } else if (node.detail !== m.type) { node.detail = 'unknown'; node.info = 'multiple different definitions for this metric'; } if (node.info === '') { node.info = m.help; } else if (node.info !== m.help) { node.info = 'multiple different definitions for this metric'; } } } else if (metadata.length === 1) { let { type, help } = metadata[0]; if (type === 'histogram' || type === 'summary') { if (metricName.endsWith('_count')) { type = 'counter'; help = `The total number of observations for: ${help}`; } if (metricName.endsWith('_sum')) { type = 'counter'; help = `The total sum of observations for: ${help}`; } if (metricName.endsWith('_bucket')) { type = 'counter'; help = `The total count of observations for a bucket in the histogram: ${help}`; } } node.detail = type; node.info = help; } } } } return result.concat(Array.from(metricCompletion.values())); }); } autocompleteLabelName(result, context) { if (!this.prometheusClient) { return result; } return this.prometheusClient.labelNames(context.metricName, context.matchers).then((labelNames) => { return result.concat(labelNames.map((value) => ({ label: value, type: 'none' }))); }); } autocompleteLabelValue(result, context) { if (!this.prometheusClient || !context.labelName) { return result; } return this.prometheusClient.labelValues(context.labelName, context.metricName, context.matchers).then((labelValues) => { return result.concat(labelValues.map((value) => ({ label: value, type: 'none' }))); }); } } //# sourceMappingURL=hybrid.js.map