diff --git a/CHANGELOG.md b/CHANGELOG.md index a8c6710..a52adf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog for grafana-metric +## Unreleased + +### Features / enhancements + +- Add method to filter by series name, field name and multiple labels [#35](https://github.com/gapitio/grafana-metric/pull/35) + ## v1.1.1 (2022/01/07) ### Features / enhancements diff --git a/src/index.ts b/src/index.ts index e580b43..02e1f59 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,4 @@ export * from "./utils/getDataFieldsFromName"; export * from "./utils/getFieldFromName"; export * from "./utils/getSeriesFromName"; export * from "./utils/evaluateString"; +export * from "./utils/getMetricValueWithFilters"; diff --git a/src/utils/getDataFieldsFromName.ts b/src/utils/getDataFieldsFromName.ts index ea0645b..e6c0766 100644 --- a/src/utils/getDataFieldsFromName.ts +++ b/src/utils/getDataFieldsFromName.ts @@ -77,7 +77,7 @@ function getSeriesAndValueField( export function getDataFieldsFromName( name: string, { searchLabels = true, getTime = true }: DataFieldOptions = {} -): DataFields | Record { +): DataFields { const { series, valueField } = getSeriesAndValueField(name, { searchLabels }); if (series && valueField) return { diff --git a/src/utils/getMetricValueWithFilters/getMetricValueWithFilters.test.ts b/src/utils/getMetricValueWithFilters/getMetricValueWithFilters.test.ts new file mode 100644 index 0000000..a398b30 --- /dev/null +++ b/src/utils/getMetricValueWithFilters/getMetricValueWithFilters.test.ts @@ -0,0 +1,227 @@ +import { FieldType, LoadingState, PanelData, dateTime } from "@grafana/data"; + +import { TIME_FIELD, field } from "../field"; + +import { getMetricValueWithFilters } from "./getMetricValueWithFilters"; + +declare global { + interface Window { + data?: PanelData; + } +} + +window.data = { + state: LoadingState.Done, + series: [ + { + name: "series-1", + fields: [ + TIME_FIELD, + field({ + name: "Value", + type: FieldType.number, + values: [47, 100], + labels: { label1: "label1" }, + }), + ], + length: 1, + }, + { + name: "series-2", + fields: [ + TIME_FIELD, + field({ + name: "Value", + type: FieldType.number, + values: [200], + labels: { label2: "label2" }, + }), + ], + length: 1, + }, + { + name: "series-3", + fields: [ + TIME_FIELD, + field({ + name: "FieldName", + type: FieldType.number, + values: [300], + labels: { label3: "label3" }, + }), + ], + length: 1, + }, + { + name: "series-3", + fields: [ + TIME_FIELD, + field({ + name: "FieldName", + type: FieldType.number, + values: [400], + labels: { label3: "label3", label4: "first" }, + }), + ], + length: 1, + }, + { + name: "series-3", + fields: [ + TIME_FIELD, + field({ + name: "FieldName", + type: FieldType.number, + values: [500], + labels: { label3: "label3", label4: "second" }, + }), + ], + length: 1, + }, + { + name: "series-12", + fields: [ + TIME_FIELD, + field({ + name: "FieldName", + type: FieldType.number, + values: [100, 200, 300, 400, 500], + labels: { label3: "label3", label4: "second" }, + }), + ], + length: 5, + }, + ], + timeRange: { + from: dateTime(0), + to: dateTime(0), + raw: { + from: dateTime(0), + to: dateTime(0), + }, + }, +}; + +describe("getMetricValue", () => { + beforeEach(() => { + jest.spyOn(global.Math, "random").mockReturnValue(0.5); + }); + + afterEach(() => { + jest.spyOn(global.Math, "random").mockRestore(); + }); + + it("retrieves random value", () => { + expect( + getMetricValueWithFilters({ seriesName: "series-1", showcase: true }) + ).toEqual(500); + expect( + getMetricValueWithFilters({ + seriesName: "series-1", + showcase: true, + range: { min: 0, max: 10 }, + decimals: 2, + }) + ).toEqual(5); + }); + + it("retrieves metric value w/o set fieldName & labels", () => { + expect(getMetricValueWithFilters({ seriesName: "series-1" })).toEqual(100); + }); + + it("retrieves metric value w/o set fieldName", () => { + expect( + getMetricValueWithFilters({ + seriesName: "series-1", + labels: { label1: "label1" }, + }) + ).toEqual(100); + }); + + it("retrieves metric value with all variables", () => { + expect( + getMetricValueWithFilters({ + seriesName: "series-3", + fieldName: "FieldName", + labels: { label3: "label3" }, + }) + ).toEqual(300); + }); + + it("retrieves first object when multiple is available", () => { + expect( + getMetricValueWithFilters({ + seriesName: "series-3", + fieldName: "FieldName", + labels: { label3: "label3" }, + }) + ).toEqual(300); + }); + + it("retrieves null when no match is found", () => { + expect( + getMetricValueWithFilters({ + seriesName: "series-4", + fieldName: "FieldName", + labels: { label3: "label3" }, + }) + ).toEqual(null); + }); + + it("retrieves null when including existing and nonexisting labels", () => { + expect( + getMetricValueWithFilters({ + seriesName: "series-3", + fieldName: "FieldName", + labels: { label3: "label3", nonExistent: "nonExistent" }, + }) + ).toEqual(null); + }); + + it("retrieves 'No data' when including noDataValue = 'No data'", () => { + expect( + getMetricValueWithFilters({ + seriesName: "series-3", + fieldName: "FieldName", + labels: { label3: "label3", nonExistent: "nonExistent" }, + noDataValue: "No data", + }) + ).toEqual("No data"); + }); + + it("retrieves correct metric value when multiple labels", () => { + expect( + getMetricValueWithFilters({ + seriesName: "series-3", + fieldName: "FieldName", + labels: { label3: "label3", label4: "first" }, + }) + ).toEqual(400); + expect( + getMetricValueWithFilters({ + seriesName: "series-3", + fieldName: "FieldName", + labels: { label3: "label3", label4: "second" }, + }) + ).toEqual(500); + }); + + it("retrieves correct metric value with reducerId", () => { + expect( + getMetricValueWithFilters({ + seriesName: "series-12", + fieldName: "FieldName", + labels: { label3: "label3" }, + reducerID: "first", + }) + ).toEqual(100); + expect( + getMetricValueWithFilters({ + seriesName: "series-12", + fieldName: "FieldName", + labels: { label3: "label3" }, + reducerID: "last", + }) + ).toEqual(500); + }); +}); diff --git a/src/utils/getMetricValueWithFilters/getMetricValueWithFilters.ts b/src/utils/getMetricValueWithFilters/getMetricValueWithFilters.ts new file mode 100644 index 0000000..6a637a0 --- /dev/null +++ b/src/utils/getMetricValueWithFilters/getMetricValueWithFilters.ts @@ -0,0 +1,123 @@ +import { DataFrame, Field, PanelData } from "@grafana/data"; + +import { ReducerID } from "../field"; +import { getValue } from "../metricValue"; +import { getShowcaseMetricValue } from "../metricValue/getShowcaseMetricValue"; + +declare const data: PanelData; + +function getMultipleSeriesFromName(seriesName: string): DataFrame[] { + return data.series.filter((series) => series.name === seriesName); +} + +function getValueFieldsFromSeries(series: DataFrame[], fieldName: string) { + return series + .map(({ fields }) => + fields.find((field) => + [field.name, field.state?.displayName].includes(fieldName) + ) + ) + .filter((x): x is Exclude => x !== undefined); +} + +function getValueFieldsFromName(metricName: string, fieldName: string) { + const series = getMultipleSeriesFromName(metricName); + return getValueFieldsFromSeries(series, fieldName); +} + +function getValueFieldFromLabels( + valueFields: Field[], + labels: Record +) { + return valueFields.find(({ labels: fieldLabels }) => + Object.entries(labels).every( + ([k, v]) => fieldLabels && fieldLabels[k] === v + ) + ); +} + +function getMetricSeries({ + seriesName, + fieldName, + labels, + noDataValue, + reducerID, +}: { + seriesName: string; + fieldName: string; + labels: Record; + noDataValue: unknown; + reducerID: string; +}) { + const valueFields = getValueFieldsFromName(seriesName, fieldName); + const valueField = getValueFieldFromLabels(valueFields, labels); + const value = + valueField?.state?.calcs?.[reducerID] ?? getValue(valueField, reducerID); + return value ?? noDataValue; +} + +/** + * Function provides value from grafana queries based on given filters. + * + * @example + * + * getMetricValueWithFilters({seriesName: "series-1"}); // Returns first series with name "series-1". + * + * getMetricValueWithFilters({ + seriesName: "series-1", + fieldName: "field-1", + }); // Returns first series with name "series-1" and fieldName "field-1". + * + * getMetricValueWithFilters({ + seriesName: "series-1", + fieldName: "field-1", + labels: { someLabelKey: "label-1", anotherLabelKey: "label-2" }, + }) // Returns first series with name "series-1", fieldName "field-1" and field contains matching labels. + * + * // Showcase + * getMetricValueWithFilters({ seriesName: "series-1", showcase: true }) // Returns a random value between 0 and 1000. + * + * getMetricValueWithFilters({ + seriesName: "series-1", + showcase: true, + range: { min: 0, max: 10 }, + decimals: 2, + }) // Returns random value between 1-10. + * + * @param seriesName - String for identifying correct series in Grafana's data object. + * @param fieldName - String for identifying correct field in series(Defaults to "Value"). + * @param labels - Object for filtering a specific series when multiple series have the same seriesName. + * @param showcase - Boolean for returning random showcase value. + * @param range - Object for setting minimum(min:) or maximum(max:) range for showcase value. + * @param decimals - Number for setting static amount of decimals for showcase value. + */ +export function getMetricValueWithFilters({ + seriesName, + fieldName = "Value", + labels = {}, + showcase, + range, + decimals, + noDataValue = null, + reducerID = ReducerID.last, +}: { + seriesName: string; + fieldName?: string; + labels?: Record; + showcase?: boolean; + range?: { min: number; max: number }; + decimals?: number; + noDataValue?: unknown; + reducerID?: string; +}): unknown { + if (showcase) { + return getShowcaseMetricValue({ range, decimals }); + } + return getMetricSeries({ + seriesName, + fieldName, + labels, + noDataValue, + reducerID, + }); +} diff --git a/src/utils/getMetricValueWithFilters/index.ts b/src/utils/getMetricValueWithFilters/index.ts new file mode 100644 index 0000000..90fcca6 --- /dev/null +++ b/src/utils/getMetricValueWithFilters/index.ts @@ -0,0 +1 @@ +export * from "./getMetricValueWithFilters";