mirror of https://github.com/grafana/grafana
Explore Metrics: Get OTel resources and filter metrics and labels (#91221)
* add OTel filter in metric select scene * add resource query to get matching OTEL job&instance * filter metrics by OTEL resources * only add otel select if DS has OTEL matching job and instance * add folder for otel resources * upate metric select for new otel folder * move otel api call * get otel resources for labels for single series job/instance target_info * add otel resources to adhoc variable dropdown * update otel api to check for standardization and return labels * label types for api * check standardization, show otel variable, select depenv, update other variables * remove otel target list from metric select scene * load resources if dep_env label has already been selected * exclude previously used filters * do not check standardization if there are already otel filters * drop filters when switching data sources * add experience var for switching to otel experience * remove otel from variables and place near settings * add error for non-standard prom with otel resources * fix typescript errors, remove ts-ignores * add custom variable for deployment environment like app-olly * fix name of otel variable * add function for getting otel resources from variables * add otel join query const * update standard check to be simpler * allow for unstandard otel data sources but give warning * add otelJoinQuery to the base query and clean up variables when state changes * refactor otel functions to return filters for targets, use targets to filter metrics * update metric names on otel target filter change * when no otel targets for otel resource filter, show no metrics * move switch to settings, default to use experience, refactor otel checks * clean code * fix refactor to add hasOtelResources for showing the switch in settings * sort otel resources by blessed list * reset otel when data source is changed * move otel experience toggle back outside settings * move showPreviews into settings * do not re-add otel resources from blessed list to filters when already selected * add otel join query variable to histogram base query * only show settings for appropriate scenes * show info tooltip the same but show error on hover for disabling otel exp for unstandard DS * refactor tagKeys and tagValues for otel resources variable, fix promoted list ordering, fix dep env state bug * default dep env value * apply var filters only where they are using VAR_FILTER_EXPR in queryies * change copy for labels to attributes * do not group_left job label when already joining by job * update copy for label variable when using otel * remove isStandard check for now because of data staleness in Prometheus * default to showing heatmap for histograms * add trail history for selecting dep env and otel resources * add otel resource attributes tests for DataTrail * move otel functions to utils * write tests for otel api calls * write tests for otel utils functions * fix history * standard otel has target_info metric and deployment_environment resource attributes * fix tests * refactor otel functions for updating state and variables * clean code * fix tests * fix tests * mock checkDataSourceForOtelResources * fix tests * update query tests with otelJoinQuery and default to heatmap for _bucket metrics * fix tests for otel api * fix trail history test * fix trail store tests for missing otel variables * make i18n-extract * handle target_info with inconsistent job and instance labels * fix otel copy and <Trans> component * fix custom variable deployment environment bug when switchiing data sources from non otel to otel * fix linting error for trans component * format i18nKey correctly * clean up old comments * add frontend hardening for OTel job and instance metric list filtering * fix test for deployment environment custom variable to use changeValueTo * fix i18n * remove comments for fixed bug * edit skipped testspull/93302/head
parent
542105b680
commit
4d1adf9db4
@ -1,8 +1,10 @@ |
|||||||
import { VAR_METRIC_EXPR, VAR_FILTERS_EXPR } from 'app/features/trails/shared'; |
import { VAR_METRIC_EXPR, VAR_FILTERS_EXPR, VAR_OTEL_JOIN_QUERY_EXPR } from 'app/features/trails/shared'; |
||||||
|
|
||||||
const GENERAL_BASE_QUERY = `${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}`; |
const GENERAL_BASE_QUERY = `${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}`; |
||||||
const GENERAL_RATE_BASE_QUERY = `rate(${GENERAL_BASE_QUERY}[$__rate_interval])`; |
const GENERAL_RATE_BASE_QUERY = `rate(${GENERAL_BASE_QUERY}[$__rate_interval])`; |
||||||
|
|
||||||
export function getGeneralBaseQuery(rate: boolean) { |
export function getGeneralBaseQuery(rate: boolean) { |
||||||
return rate ? GENERAL_RATE_BASE_QUERY : GENERAL_BASE_QUERY; |
return rate |
||||||
|
? `${GENERAL_RATE_BASE_QUERY} ${VAR_OTEL_JOIN_QUERY_EXPR}` |
||||||
|
: `${GENERAL_BASE_QUERY} ${VAR_OTEL_JOIN_QUERY_EXPR}`; |
||||||
} |
} |
||||||
|
|||||||
@ -0,0 +1,90 @@ |
|||||||
|
import { RawTimeRange } from '@grafana/data'; |
||||||
|
import { BackendSrvRequest } from '@grafana/runtime'; |
||||||
|
|
||||||
|
import { getOtelResources, totalOtelResources, isOtelStandardization, getDeploymentEnvironments } from './api'; |
||||||
|
|
||||||
|
jest.mock('@grafana/runtime', () => ({ |
||||||
|
getBackendSrv: () => { |
||||||
|
return { |
||||||
|
get: ( |
||||||
|
url: string, |
||||||
|
params?: Record<string, string | number>, |
||||||
|
requestId?: string, |
||||||
|
options?: Partial<BackendSrvRequest> |
||||||
|
) => { |
||||||
|
if (requestId === 'explore-metrics-otel-resources') { |
||||||
|
return Promise.resolve({ data: ['job', 'instance', 'deployment_environment'] }); |
||||||
|
} else if (requestId === 'explore-metrics-otel-check-total') { |
||||||
|
return Promise.resolve({ |
||||||
|
data: { |
||||||
|
result: [ |
||||||
|
{ metric: { job: 'job1', instance: 'instance1' } }, |
||||||
|
{ metric: { job: 'job2', instance: 'instance2' } }, |
||||||
|
], |
||||||
|
}, |
||||||
|
}); |
||||||
|
} else if (requestId === 'explore-metrics-otel-check-standard') { |
||||||
|
return Promise.resolve({ |
||||||
|
data: { |
||||||
|
result: [{ metric: { job: 'job1', instance: 'instance1' } }], |
||||||
|
}, |
||||||
|
}); |
||||||
|
} else if (requestId === 'explore-metrics-otel-resources-deployment-env') { |
||||||
|
return Promise.resolve({ data: ['env1', 'env2'] }); |
||||||
|
} |
||||||
|
return []; |
||||||
|
}, |
||||||
|
}; |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
describe('OTEL API', () => { |
||||||
|
const dataSourceUid = 'test-uid'; |
||||||
|
const timeRange: RawTimeRange = { |
||||||
|
from: 'now-1h', |
||||||
|
to: 'now', |
||||||
|
}; |
||||||
|
|
||||||
|
afterAll(() => { |
||||||
|
jest.clearAllMocks(); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('getOtelResources', () => { |
||||||
|
it('should fetch and filter OTEL resources', async () => { |
||||||
|
const resources = await getOtelResources(dataSourceUid, timeRange); |
||||||
|
|
||||||
|
expect(resources).toEqual(['job', 'instance']); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('totalOtelResources', () => { |
||||||
|
it('should fetch total OTEL resources', async () => { |
||||||
|
const result = await totalOtelResources(dataSourceUid, timeRange); |
||||||
|
|
||||||
|
expect(result).toEqual({ |
||||||
|
jobs: ['job1', 'job2'], |
||||||
|
instances: ['instance1', 'instance2'], |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('isOtelStandardization', () => { |
||||||
|
// keeping for reference because standardization for OTel by series on target_info for job&instance is not consistent
|
||||||
|
// There is a bug currently where there is stale data in Prometheus resulting in duplicate series for job&instance at random times
|
||||||
|
// When this is resolved, we can check for standardization again
|
||||||
|
xit('should check if OTEL standardization is met when there are no duplicate series on target_info for job&instance', async () => { |
||||||
|
// will return duplicates, see mock above
|
||||||
|
const isStandard = await isOtelStandardization(dataSourceUid, timeRange); |
||||||
|
|
||||||
|
expect(isStandard).toBe(false); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('getDeploymentEnvironments', () => { |
||||||
|
it('should fetch deployment environments', async () => { |
||||||
|
const environments = await getDeploymentEnvironments(dataSourceUid, timeRange); |
||||||
|
|
||||||
|
expect(environments).toEqual(['env1', 'env2']); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
@ -0,0 +1,166 @@ |
|||||||
|
import { RawTimeRange } from '@grafana/data'; |
||||||
|
import { getPrometheusTime } from '@grafana/prometheus/src/language_utils'; |
||||||
|
import { getBackendSrv } from '@grafana/runtime'; |
||||||
|
|
||||||
|
import { OtelResponse, LabelResponse, OtelTargetType } from './types'; |
||||||
|
|
||||||
|
const OTEL_RESOURCE_EXCLUDED_FILTERS = ['__name__', 'deployment_environment']; // name is handled by metric search metrics bar
|
||||||
|
/** |
||||||
|
* Function used to test for OTEL |
||||||
|
* When filters are added, we can also get a list of otel targets used to reduce the metric list |
||||||
|
* */ |
||||||
|
const otelTargetInfoQuery = (filters?: string) => `count(target_info{${filters ?? ''}}) by (job, instance)`; |
||||||
|
|
||||||
|
export const TARGET_INFO_FILTER = { key: '__name__', value: 'target_info', operator: '=' }; |
||||||
|
|
||||||
|
/** |
||||||
|
* Query the DS for target_info matching job and instance. |
||||||
|
* Parse the results to get label filters. |
||||||
|
* @param dataSourceUid |
||||||
|
* @param timeRange |
||||||
|
* @returns OtelResourcesType[], labels for the query result requesting matching job and instance on target_info metric |
||||||
|
*/ |
||||||
|
export async function getOtelResources( |
||||||
|
dataSourceUid: string, |
||||||
|
timeRange: RawTimeRange, |
||||||
|
excludedFilters?: string[], |
||||||
|
matchFilters?: string |
||||||
|
): Promise<string[]> { |
||||||
|
const allExcludedFilters = (excludedFilters ?? []).concat(OTEL_RESOURCE_EXCLUDED_FILTERS); |
||||||
|
|
||||||
|
const start = getPrometheusTime(timeRange.from, false); |
||||||
|
const end = getPrometheusTime(timeRange.to, true); |
||||||
|
|
||||||
|
const url = `/api/datasources/uid/${dataSourceUid}/resources/api/v1/labels`; |
||||||
|
const params: Record<string, string | number> = { |
||||||
|
start, |
||||||
|
end, |
||||||
|
'match[]': `{__name__="target_info"${matchFilters ? `,${matchFilters}` : ''}}`, |
||||||
|
}; |
||||||
|
|
||||||
|
const response = await getBackendSrv().get<LabelResponse>(url, params, 'explore-metrics-otel-resources'); |
||||||
|
|
||||||
|
// exclude __name__ or deployment_environment or previously chosen filters
|
||||||
|
const resources = response.data?.filter((resource) => !allExcludedFilters.includes(resource)).map((el: string) => el); |
||||||
|
|
||||||
|
return resources; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the total amount of job/instance pairs on target info metric |
||||||
|
* |
||||||
|
* @param dataSourceUid |
||||||
|
* @param timeRange |
||||||
|
* @param expr |
||||||
|
* @returns |
||||||
|
*/ |
||||||
|
export async function totalOtelResources( |
||||||
|
dataSourceUid: string, |
||||||
|
timeRange: RawTimeRange, |
||||||
|
filters?: string |
||||||
|
): Promise<OtelTargetType> { |
||||||
|
const start = getPrometheusTime(timeRange.from, false); |
||||||
|
const end = getPrometheusTime(timeRange.to, true); |
||||||
|
|
||||||
|
const url = `/api/datasources/uid/${dataSourceUid}/resources/api/v1/query`; |
||||||
|
const paramsTotalTargets: Record<string, string | number> = { |
||||||
|
start, |
||||||
|
end, |
||||||
|
query: otelTargetInfoQuery(filters), |
||||||
|
}; |
||||||
|
|
||||||
|
const responseTotal = await getBackendSrv().get<OtelResponse>( |
||||||
|
url, |
||||||
|
paramsTotalTargets, |
||||||
|
'explore-metrics-otel-check-total' |
||||||
|
); |
||||||
|
|
||||||
|
let jobs: string[] = []; |
||||||
|
let instances: string[] = []; |
||||||
|
|
||||||
|
responseTotal.data.result.forEach((result) => { |
||||||
|
// NOTE: sometimes there are target_info series with
|
||||||
|
// - both job and instance labels
|
||||||
|
// - only job label
|
||||||
|
// - only instance label
|
||||||
|
// Here we make sure both of them are present
|
||||||
|
// because we use this collection to filter metric names
|
||||||
|
if (result.metric.job && result.metric.instance) { |
||||||
|
jobs.push(result.metric.job); |
||||||
|
instances.push(result.metric.instance); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
const otelTargets: OtelTargetType = { |
||||||
|
jobs, |
||||||
|
instances, |
||||||
|
}; |
||||||
|
|
||||||
|
return otelTargets; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Look for duplicated series in target_info metric by job and instance labels |
||||||
|
* If each job&instance combo is unique, the data source is otel standardized. |
||||||
|
* If there is a count by job&instance on target_info greater than one, |
||||||
|
* the data source is not standardized |
||||||
|
* |
||||||
|
* @param dataSourceUid |
||||||
|
* @param timeRange |
||||||
|
* @param expr |
||||||
|
* @returns |
||||||
|
*/ |
||||||
|
export async function isOtelStandardization( |
||||||
|
dataSourceUid: string, |
||||||
|
timeRange: RawTimeRange, |
||||||
|
expr?: string |
||||||
|
): Promise<boolean> { |
||||||
|
const url = `/api/datasources/uid/${dataSourceUid}/resources/api/v1/query`; |
||||||
|
|
||||||
|
const start = getPrometheusTime(timeRange.from, false); |
||||||
|
const end = getPrometheusTime(timeRange.to, true); |
||||||
|
|
||||||
|
const paramsTargets: Record<string, string | number> = { |
||||||
|
start, |
||||||
|
end, |
||||||
|
// any data source with duplicated series will have a count > 1
|
||||||
|
query: `${otelTargetInfoQuery()} > 1`, |
||||||
|
}; |
||||||
|
|
||||||
|
const response = await getBackendSrv().get<OtelResponse>(url, paramsTargets, 'explore-metrics-otel-check-standard'); |
||||||
|
|
||||||
|
// the response should be not greater than zero if it is standard
|
||||||
|
const checkStandard = !(response.data.result.length > 0); |
||||||
|
|
||||||
|
return checkStandard; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Query the DS for deployment environment label values. |
||||||
|
* |
||||||
|
* @param dataSourceUid |
||||||
|
* @param timeRange |
||||||
|
* @returns string[], values for the deployment_environment label |
||||||
|
*/ |
||||||
|
export async function getDeploymentEnvironments(dataSourceUid: string, timeRange: RawTimeRange): Promise<string[]> { |
||||||
|
const start = getPrometheusTime(timeRange.from, false); |
||||||
|
const end = getPrometheusTime(timeRange.to, true); |
||||||
|
|
||||||
|
const url = `/api/datasources/uid/${dataSourceUid}/resources/api/v1/label/deployment_environment/values`; |
||||||
|
const params: Record<string, string | number> = { |
||||||
|
start, |
||||||
|
end, |
||||||
|
'match[]': '{__name__="target_info"}', |
||||||
|
}; |
||||||
|
|
||||||
|
const response = await getBackendSrv().get<LabelResponse>( |
||||||
|
url, |
||||||
|
params, |
||||||
|
'explore-metrics-otel-resources-deployment-env' |
||||||
|
); |
||||||
|
|
||||||
|
// exclude __name__ or deployment_environment or previously chosen filters
|
||||||
|
const resources = response.data; |
||||||
|
|
||||||
|
return resources; |
||||||
|
} |
||||||
@ -0,0 +1,32 @@ |
|||||||
|
export type OtelResponse = { |
||||||
|
data: { |
||||||
|
result: [ |
||||||
|
{ |
||||||
|
metric: { |
||||||
|
job: string; |
||||||
|
instance: string; |
||||||
|
}; |
||||||
|
}, |
||||||
|
]; |
||||||
|
}; |
||||||
|
status: 'success' | 'error'; |
||||||
|
error?: 'string'; |
||||||
|
warnings?: string[]; |
||||||
|
}; |
||||||
|
|
||||||
|
export type LabelResponse = { |
||||||
|
data: string[]; |
||||||
|
status: 'success' | 'error'; |
||||||
|
error?: 'string'; |
||||||
|
warnings?: string[]; |
||||||
|
}; |
||||||
|
|
||||||
|
export type OtelTargetType = { |
||||||
|
jobs: string[]; |
||||||
|
instances: string[]; |
||||||
|
}; |
||||||
|
|
||||||
|
export type OtelResourcesObject = { |
||||||
|
filters: string; |
||||||
|
labels: string; |
||||||
|
}; |
||||||
@ -0,0 +1,195 @@ |
|||||||
|
import { MetricFindValue } from '@grafana/data'; |
||||||
|
import { AdHocFiltersVariable, CustomVariable, sceneGraph, SceneObject } from '@grafana/scenes'; |
||||||
|
|
||||||
|
import { VAR_OTEL_DEPLOYMENT_ENV, VAR_OTEL_RESOURCES } from '../shared'; |
||||||
|
|
||||||
|
import { OtelResourcesObject } from './types'; |
||||||
|
|
||||||
|
export const blessedList = (): Record<string, number> => { |
||||||
|
return { |
||||||
|
cloud_availability_zone: 0, |
||||||
|
cloud_region: 0, |
||||||
|
container_name: 0, |
||||||
|
k8s_cluster_name: 0, |
||||||
|
k8s_container_name: 0, |
||||||
|
k8s_cronjob_name: 0, |
||||||
|
k8s_daemonset_name: 0, |
||||||
|
k8s_deployment_name: 0, |
||||||
|
k8s_job_name: 0, |
||||||
|
k8s_namespace_name: 0, |
||||||
|
k8s_pod_name: 0, |
||||||
|
k8s_replicaset_name: 0, |
||||||
|
k8s_statefulset_name: 0, |
||||||
|
service_instance_id: 0, |
||||||
|
service_name: 0, |
||||||
|
service_namespace: 0, |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
export function sortResources(resources: MetricFindValue[], excluded: string[]) { |
||||||
|
// these may be filtered
|
||||||
|
const promotedList = blessedList(); |
||||||
|
|
||||||
|
const blessed = Object.keys(promotedList); |
||||||
|
|
||||||
|
resources = resources.filter((resource) => { |
||||||
|
// if not in the list keep it
|
||||||
|
const val = (resource.value ?? '').toString(); |
||||||
|
|
||||||
|
if (!blessed.includes(val)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
// remove blessed filters
|
||||||
|
// but indicate which are available
|
||||||
|
promotedList[val] = 1; |
||||||
|
return false; |
||||||
|
}); |
||||||
|
|
||||||
|
const promotedResources = Object.keys(promotedList) |
||||||
|
.filter((resource) => promotedList[resource] && !excluded.includes(resource)) |
||||||
|
.map((v) => ({ text: v })); |
||||||
|
|
||||||
|
// put the filters first
|
||||||
|
return promotedResources.concat(resources); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a collection of labels and labels filters. |
||||||
|
* This data is used to build the join query to filter with otel resources |
||||||
|
* |
||||||
|
* @param otelResourcesObject |
||||||
|
* @returns a string that is used to add a join query to filter otel resources |
||||||
|
*/ |
||||||
|
export function getOtelJoinQuery(otelResourcesObject: OtelResourcesObject): string { |
||||||
|
let otelResourcesJoinQuery = ''; |
||||||
|
if (otelResourcesObject.filters && otelResourcesObject.labels) { |
||||||
|
// add support for otel data sources that are not standardized, i.e., have non unique target_info series by job, instance
|
||||||
|
otelResourcesJoinQuery = `* on (job, instance) group_left(${otelResourcesObject.labels}) topk by (job, instance) (1, target_info{${otelResourcesObject.filters}})`; |
||||||
|
} |
||||||
|
|
||||||
|
return otelResourcesJoinQuery; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns an object containing all the filters for otel resources as well as a list of labels |
||||||
|
* |
||||||
|
* @param scene |
||||||
|
* @param firstQueryVal |
||||||
|
* @returns |
||||||
|
*/ |
||||||
|
export function getOtelResourcesObject(scene: SceneObject, firstQueryVal?: string): OtelResourcesObject { |
||||||
|
const otelResources = sceneGraph.lookupVariable(VAR_OTEL_RESOURCES, scene); |
||||||
|
// add deployment env to otel resource filters
|
||||||
|
const otelDepEnv = sceneGraph.lookupVariable(VAR_OTEL_DEPLOYMENT_ENV, scene); |
||||||
|
|
||||||
|
let otelResourcesObject = { labels: '', filters: '' }; |
||||||
|
|
||||||
|
if (otelResources instanceof AdHocFiltersVariable && otelDepEnv instanceof CustomVariable) { |
||||||
|
// get the collection of adhoc filters
|
||||||
|
const otelFilters = otelResources.state.filters; |
||||||
|
|
||||||
|
// get the value for deployment_environment variable
|
||||||
|
let otelDepEnvValue = String(otelDepEnv.getValue()); |
||||||
|
// check if there are multiple environments
|
||||||
|
const isMulti = otelDepEnvValue.includes(','); |
||||||
|
// start with the default label filters for deployment_environment
|
||||||
|
let op = '='; |
||||||
|
let val = firstQueryVal ? firstQueryVal : otelDepEnvValue; |
||||||
|
// update the filters if multiple deployment environments selected
|
||||||
|
if (isMulti) { |
||||||
|
op = '=~'; |
||||||
|
val = val.split(',').join('|'); |
||||||
|
} |
||||||
|
|
||||||
|
// start with the deployment environment
|
||||||
|
let allFilters = `deployment_environment${op}"${val}"`; |
||||||
|
let allLabels = 'deployment_environment'; |
||||||
|
|
||||||
|
// add the other OTEL resource filters
|
||||||
|
for (let i = 0; i < otelFilters?.length; i++) { |
||||||
|
const labelName = otelFilters[i].key; |
||||||
|
const op = otelFilters[i].operator; |
||||||
|
const labelValue = otelFilters[i].value; |
||||||
|
|
||||||
|
allFilters += `,${labelName}${op}"${labelValue}"`; |
||||||
|
|
||||||
|
const addLabelToGroupLeft = labelName !== 'job' && labelName !== 'instance'; |
||||||
|
|
||||||
|
if (addLabelToGroupLeft) { |
||||||
|
allLabels += `,${labelName}`; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
otelResourcesObject.labels = allLabels; |
||||||
|
otelResourcesObject.filters = allFilters; |
||||||
|
|
||||||
|
return otelResourcesObject; |
||||||
|
} |
||||||
|
return otelResourcesObject; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This function checks that when adding OTel job and instance filters |
||||||
|
* to the label values request for a list of metrics, |
||||||
|
* the total character count of the request does not exceed 2000 characters |
||||||
|
* |
||||||
|
* @param matchTerms __name__ and other Prom filters |
||||||
|
* @param jobsList list of jobs in target_info |
||||||
|
* @param instancesList list of instances in target_info |
||||||
|
* @param missingOtelTargets flag to indicate truncated job and instance filters |
||||||
|
* @returns |
||||||
|
*/ |
||||||
|
export function limitOtelMatchTerms( |
||||||
|
matchTerms: string[], |
||||||
|
jobsList: string[], |
||||||
|
instancesList: string[], |
||||||
|
missingOtelTargets: boolean |
||||||
|
): { missingOtelTargets: boolean; jobsRegex: string; instancesRegex: string } { |
||||||
|
const charLimit = 2000; |
||||||
|
|
||||||
|
let initialCharAmount = matchTerms.join(',').length; |
||||||
|
|
||||||
|
// start to add values to the regex and start quote
|
||||||
|
let jobsRegex = 'job=~"'; |
||||||
|
let instancesRegex = 'instance=~"'; |
||||||
|
|
||||||
|
// iterate through the jobs and instances,
|
||||||
|
// count the chars as they are added,
|
||||||
|
// stop before the total count reaches 2000
|
||||||
|
// show a warning that there are missing OTel targets and
|
||||||
|
// the user must select more OTel resource attributes
|
||||||
|
for (let i = 0; i < jobsList.length; i++) { |
||||||
|
// use or character for the count
|
||||||
|
const orChars = i === 0 ? 0 : 2; |
||||||
|
// count all the characters that will go into the match terms
|
||||||
|
const checkCharAmount = |
||||||
|
initialCharAmount + |
||||||
|
jobsRegex.length + |
||||||
|
jobsList[i].length + |
||||||
|
instancesRegex.length + |
||||||
|
instancesList[i].length + |
||||||
|
orChars; |
||||||
|
|
||||||
|
if (checkCharAmount <= charLimit) { |
||||||
|
if (i === 0) { |
||||||
|
jobsRegex += `${jobsList[i]}`; |
||||||
|
instancesRegex += `${instancesList[i]}`; |
||||||
|
} else { |
||||||
|
jobsRegex += `|${jobsList[i]}`; |
||||||
|
instancesRegex += `|${instancesList[i]}`; |
||||||
|
} |
||||||
|
} else { |
||||||
|
missingOtelTargets = true; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
// complete the quote after values have been added
|
||||||
|
jobsRegex += '"'; |
||||||
|
instancesRegex += '"'; |
||||||
|
|
||||||
|
return { |
||||||
|
missingOtelTargets, |
||||||
|
jobsRegex, |
||||||
|
instancesRegex, |
||||||
|
}; |
||||||
|
} |
||||||
@ -0,0 +1,191 @@ |
|||||||
|
import { MetricFindValue } from '@grafana/data'; |
||||||
|
|
||||||
|
import { sortResources, getOtelJoinQuery, blessedList, limitOtelMatchTerms } from './util'; |
||||||
|
|
||||||
|
describe('sortResources', () => { |
||||||
|
it('should sort and filter resources correctly', () => { |
||||||
|
const resources: MetricFindValue[] = [ |
||||||
|
{ text: 'cloud_region', value: 'cloud_region' }, |
||||||
|
{ text: 'custom_resource', value: 'custom_resource' }, |
||||||
|
]; |
||||||
|
const excluded: string[] = ['cloud_region']; |
||||||
|
|
||||||
|
const result = sortResources(resources, excluded); |
||||||
|
|
||||||
|
expect(result).toEqual([{ text: 'custom_resource', value: 'custom_resource' }]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('getOtelJoinQuery', () => { |
||||||
|
it('should return the correct join query', () => { |
||||||
|
const otelResourcesObject = { |
||||||
|
filters: 'job="test-job",instance="test-instance"', |
||||||
|
labels: 'deployment_environment,custom_label', |
||||||
|
}; |
||||||
|
|
||||||
|
const result = getOtelJoinQuery(otelResourcesObject); |
||||||
|
|
||||||
|
expect(result).toBe( |
||||||
|
'* on (job, instance) group_left(deployment_environment,custom_label) topk by (job, instance) (1, target_info{job="test-job",instance="test-instance"})' |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should return an empty string if filters or labels are missing', () => { |
||||||
|
const otelResourcesObject = { |
||||||
|
filters: '', |
||||||
|
labels: '', |
||||||
|
}; |
||||||
|
|
||||||
|
const result = getOtelJoinQuery(otelResourcesObject); |
||||||
|
|
||||||
|
expect(result).toBe(''); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('blessedList', () => { |
||||||
|
it('should return the correct blessed list', () => { |
||||||
|
const result = blessedList(); |
||||||
|
expect(result).toEqual({ |
||||||
|
cloud_availability_zone: 0, |
||||||
|
cloud_region: 0, |
||||||
|
container_name: 0, |
||||||
|
k8s_cluster_name: 0, |
||||||
|
k8s_container_name: 0, |
||||||
|
k8s_cronjob_name: 0, |
||||||
|
k8s_daemonset_name: 0, |
||||||
|
k8s_deployment_name: 0, |
||||||
|
k8s_job_name: 0, |
||||||
|
k8s_namespace_name: 0, |
||||||
|
k8s_pod_name: 0, |
||||||
|
k8s_replicaset_name: 0, |
||||||
|
k8s_statefulset_name: 0, |
||||||
|
service_instance_id: 0, |
||||||
|
service_name: 0, |
||||||
|
service_namespace: 0, |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('sortResources', () => { |
||||||
|
it('should sort and filter resources correctly', () => { |
||||||
|
const resources: MetricFindValue[] = [ |
||||||
|
{ text: 'cloud_region', value: 'cloud_region' }, |
||||||
|
{ text: 'custom_resource', value: 'custom_resource' }, |
||||||
|
]; |
||||||
|
const excluded: string[] = ['cloud_region']; |
||||||
|
|
||||||
|
const result = sortResources(resources, excluded); |
||||||
|
|
||||||
|
expect(result).toEqual([{ text: 'custom_resource', value: 'custom_resource' }]); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should promote blessed resources and exclude specified ones', () => { |
||||||
|
const resources: MetricFindValue[] = [ |
||||||
|
{ text: 'custom_resource', value: 'custom_resource' }, |
||||||
|
{ text: 'k8s_cluster_name', value: 'k8s_cluster_name' }, |
||||||
|
]; |
||||||
|
const excluded: string[] = ['k8s_cluster_name']; |
||||||
|
|
||||||
|
const result = sortResources(resources, excluded); |
||||||
|
|
||||||
|
expect(result).toEqual([{ text: 'custom_resource', value: 'custom_resource' }]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('getOtelJoinQuery', () => { |
||||||
|
it('should return the correct join query', () => { |
||||||
|
const otelResourcesObject = { |
||||||
|
filters: 'job="test-job",instance="test-instance"', |
||||||
|
labels: 'deployment_environment,custom_label', |
||||||
|
}; |
||||||
|
|
||||||
|
const result = getOtelJoinQuery(otelResourcesObject); |
||||||
|
|
||||||
|
expect(result).toBe( |
||||||
|
'* on (job, instance) group_left(deployment_environment,custom_label) topk by (job, instance) (1, target_info{job="test-job",instance="test-instance"})' |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should return an empty string if filters or labels are missing', () => { |
||||||
|
const otelResourcesObject = { |
||||||
|
filters: '', |
||||||
|
labels: '', |
||||||
|
}; |
||||||
|
|
||||||
|
const result = getOtelJoinQuery(otelResourcesObject); |
||||||
|
|
||||||
|
expect(result).toBe(''); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('limitOtelMatchTerms', () => { |
||||||
|
it('should limit the OTel match terms if the total match term character count exceeds 2000', () => { |
||||||
|
// the initial match is 1980 characters
|
||||||
|
const promMatchTerms: string[] = [ |
||||||
|
`${[...Array(1979).keys()] |
||||||
|
.map((el) => { |
||||||
|
return '0'; |
||||||
|
}) |
||||||
|
.join('')}"`,
|
||||||
|
]; |
||||||
|
// job=~"" is 7 chars
|
||||||
|
// instance=~"" is 12 characters
|
||||||
|
|
||||||
|
// 7 + 12 + 1979 = 1998
|
||||||
|
// so we have room to add 2 more characters
|
||||||
|
// attribute values that are b will be left out
|
||||||
|
const jobs = ['a', 'b', 'c']; |
||||||
|
const instances = ['d', 'e', 'f']; |
||||||
|
|
||||||
|
const missingOtelTargets = false; |
||||||
|
|
||||||
|
const result = limitOtelMatchTerms(promMatchTerms, jobs, instances, missingOtelTargets); |
||||||
|
|
||||||
|
expect(result.missingOtelTargets).toEqual(true); |
||||||
|
expect(result.jobsRegex).toEqual('job=~"a"'); |
||||||
|
expect(result.instancesRegex).toEqual('instance=~"d"'); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should include | char in the count', () => { |
||||||
|
// the initial match is 1980 characters
|
||||||
|
const promMatchTerms: string[] = [ |
||||||
|
`${[...Array(1975).keys()] |
||||||
|
.map((el) => { |
||||||
|
return '0'; |
||||||
|
}) |
||||||
|
.join('')}"`,
|
||||||
|
]; |
||||||
|
// job=~"" is 7 chars
|
||||||
|
// instance=~"" is 12 characters
|
||||||
|
|
||||||
|
// 7 + 12 + 1975 = 1994
|
||||||
|
// so we have room to add 6 more characters
|
||||||
|
// the extra 6 characters will be 'a|b' and 'd|e'
|
||||||
|
const jobs = ['a', 'b', 'c']; |
||||||
|
const instances = ['d', 'e', 'f']; |
||||||
|
|
||||||
|
const missingOtelTargets = false; |
||||||
|
|
||||||
|
const result = limitOtelMatchTerms(promMatchTerms, jobs, instances, missingOtelTargets); |
||||||
|
|
||||||
|
expect(result.missingOtelTargets).toEqual(true); |
||||||
|
expect(result.jobsRegex).toEqual('job=~"a|b"'); |
||||||
|
expect(result.instancesRegex).toEqual('instance=~"d|e"'); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should add all OTel job and instance matches if the character count is less that 2000', () => { |
||||||
|
const promMatchTerms: string[] = []; |
||||||
|
|
||||||
|
const jobs = ['job1', 'job2', 'job3', 'job4', 'job5']; |
||||||
|
|
||||||
|
const instances = ['instance1', 'instance2', 'instance3', 'instance4', 'instance5']; |
||||||
|
|
||||||
|
const missingOtelTargets = false; |
||||||
|
|
||||||
|
const result = limitOtelMatchTerms(promMatchTerms, jobs, instances, missingOtelTargets); |
||||||
|
|
||||||
|
expect(result.missingOtelTargets).toEqual(false); |
||||||
|
expect(result.jobsRegex).toEqual('job=~"job1|job2|job3|job4|job5"'); |
||||||
|
expect(result.instancesRegex).toEqual('instance=~"instance1|instance2|instance3|instance4|instance5"'); |
||||||
|
}); |
||||||
|
}); |
||||||
Loading…
Reference in new issue