1import { 2 DataQuery, 3 rangeUtil, 4 RelativeTimeRange, 5 ScopedVars, 6 getDefaultRelativeTimeRange, 7 TimeRange, 8 IntervalValues, 9 DataSourceRef, 10} from '@grafana/data'; 11import { getDataSourceSrv } from '@grafana/runtime'; 12import { contextSrv } from 'app/core/services/context_srv'; 13import { getNextRefIdChar } from 'app/core/utils/query'; 14import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; 15import { ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource'; 16import { ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types'; 17import { RuleWithLocation } from 'app/types/unified-alerting'; 18import { 19 Annotations, 20 GrafanaAlertStateDecision, 21 AlertQuery, 22 Labels, 23 PostableRuleGrafanaRuleDTO, 24 RulerRuleDTO, 25} from 'app/types/unified-alerting-dto'; 26import { EvalFunction } from '../../state/alertDef'; 27import { RuleFormType, RuleFormValues } from '../types/rule-form'; 28import { Annotation } from './constants'; 29import { isGrafanaRulesSource } from './datasource'; 30import { arrayToRecord, recordToArray } from './misc'; 31import { isAlertingRulerRule, isGrafanaRulerRule, isRecordingRulerRule } from './rules'; 32import { parseInterval } from './time'; 33import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend'; 34 35export const getDefaultFormValues = (): RuleFormValues => 36 Object.freeze({ 37 name: '', 38 labels: [{ key: '', value: '' }], 39 annotations: [ 40 { key: Annotation.summary, value: '' }, 41 { key: Annotation.description, value: '' }, 42 { key: Annotation.runbookURL, value: '' }, 43 ], 44 dataSourceName: null, 45 type: !contextSrv.isEditor ? RuleFormType.grafana : undefined, // viewers can't create prom alerts 46 47 // grafana 48 folder: null, 49 queries: [], 50 condition: '', 51 noDataState: GrafanaAlertStateDecision.NoData, 52 execErrState: GrafanaAlertStateDecision.Alerting, 53 evaluateEvery: '1m', 54 evaluateFor: '5m', 55 56 // cortex / loki 57 group: '', 58 namespace: '', 59 expression: '', 60 forTime: 1, 61 forTimeUnit: 'm', 62 }); 63 64export function formValuesToRulerRuleDTO(values: RuleFormValues): RulerRuleDTO { 65 const { name, expression, forTime, forTimeUnit, type } = values; 66 if (type === RuleFormType.cloudAlerting) { 67 return { 68 alert: name, 69 for: `${forTime}${forTimeUnit}`, 70 annotations: arrayToRecord(values.annotations || []), 71 labels: arrayToRecord(values.labels || []), 72 expr: expression, 73 }; 74 } else if (type === RuleFormType.cloudRecording) { 75 return { 76 record: name, 77 labels: arrayToRecord(values.labels || []), 78 expr: expression, 79 }; 80 } 81 throw new Error(`unexpected rule type: ${type}`); 82} 83 84function listifyLabelsOrAnnotations(item: Labels | Annotations | undefined): Array<{ key: string; value: string }> { 85 return [...recordToArray(item || {}), { key: '', value: '' }]; 86} 87 88export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): PostableRuleGrafanaRuleDTO { 89 const { name, condition, noDataState, execErrState, evaluateFor, queries } = values; 90 if (condition) { 91 return { 92 grafana_alert: { 93 title: name, 94 condition, 95 no_data_state: noDataState, 96 exec_err_state: execErrState, 97 data: queries, 98 }, 99 for: evaluateFor, 100 annotations: arrayToRecord(values.annotations || []), 101 labels: arrayToRecord(values.labels || []), 102 }; 103 } 104 throw new Error('Cannot create rule without specifying alert condition'); 105} 106 107export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleFormValues { 108 const { ruleSourceName, namespace, group, rule } = ruleWithLocation; 109 110 const defaultFormValues = getDefaultFormValues(); 111 if (isGrafanaRulesSource(ruleSourceName)) { 112 if (isGrafanaRulerRule(rule)) { 113 const ga = rule.grafana_alert; 114 return { 115 ...defaultFormValues, 116 name: ga.title, 117 type: RuleFormType.grafana, 118 evaluateFor: rule.for || '0', 119 evaluateEvery: group.interval || defaultFormValues.evaluateEvery, 120 noDataState: ga.no_data_state, 121 execErrState: ga.exec_err_state, 122 queries: ga.data, 123 condition: ga.condition, 124 annotations: listifyLabelsOrAnnotations(rule.annotations), 125 labels: listifyLabelsOrAnnotations(rule.labels), 126 folder: { title: namespace, id: ga.namespace_id }, 127 }; 128 } else { 129 throw new Error('Unexpected type of rule for grafana rules source'); 130 } 131 } else { 132 if (isAlertingRulerRule(rule)) { 133 const [forTime, forTimeUnit] = rule.for 134 ? parseInterval(rule.for) 135 : [defaultFormValues.forTime, defaultFormValues.forTimeUnit]; 136 return { 137 ...defaultFormValues, 138 name: rule.alert, 139 type: RuleFormType.cloudAlerting, 140 dataSourceName: ruleSourceName, 141 namespace, 142 group: group.name, 143 expression: rule.expr, 144 forTime, 145 forTimeUnit, 146 annotations: listifyLabelsOrAnnotations(rule.annotations), 147 labels: listifyLabelsOrAnnotations(rule.labels), 148 }; 149 } else if (isRecordingRulerRule(rule)) { 150 return { 151 ...defaultFormValues, 152 name: rule.record, 153 type: RuleFormType.cloudRecording, 154 dataSourceName: ruleSourceName, 155 namespace, 156 group: group.name, 157 expression: rule.expr, 158 labels: listifyLabelsOrAnnotations(rule.labels), 159 }; 160 } else { 161 throw new Error('Unexpected type of rule for cloud rules source'); 162 } 163 } 164} 165 166export const getDefaultQueries = (): AlertQuery[] => { 167 const dataSource = getDataSourceSrv().getInstanceSettings('default'); 168 169 if (!dataSource) { 170 return [getDefaultExpression('A')]; 171 } 172 const relativeTimeRange = getDefaultRelativeTimeRange(); 173 174 return [ 175 { 176 refId: 'A', 177 datasourceUid: dataSource.uid, 178 queryType: '', 179 relativeTimeRange, 180 model: { 181 refId: 'A', 182 hide: false, 183 }, 184 }, 185 getDefaultExpression('B'), 186 ]; 187}; 188 189const getDefaultExpression = (refId: string): AlertQuery => { 190 const model: ExpressionQuery = { 191 refId, 192 hide: false, 193 type: ExpressionQueryType.classic, 194 datasource: { 195 uid: ExpressionDatasourceUID, 196 type: ExpressionDatasourceRef.type, 197 }, 198 conditions: [ 199 { 200 type: 'query', 201 evaluator: { 202 params: [3], 203 type: EvalFunction.IsAbove, 204 }, 205 operator: { 206 type: 'and', 207 }, 208 query: { 209 params: ['A'], 210 }, 211 reducer: { 212 params: [], 213 type: 'last', 214 }, 215 }, 216 ], 217 }; 218 219 return { 220 refId, 221 datasourceUid: ExpressionDatasourceUID, 222 queryType: '', 223 model, 224 }; 225}; 226 227const dataQueriesToGrafanaQueries = async ( 228 queries: DataQuery[], 229 relativeTimeRange: RelativeTimeRange, 230 scopedVars: ScopedVars | {}, 231 panelDataSourceRef?: DataSourceRef, 232 maxDataPoints?: number, 233 minInterval?: string 234): Promise<AlertQuery[]> => { 235 const result: AlertQuery[] = []; 236 237 for (const target of queries) { 238 const datasource = await getDataSourceSrv().get(target.datasource?.uid ? target.datasource : panelDataSourceRef); 239 const dsRef = { uid: datasource.uid, type: datasource.type }; 240 241 const range = rangeUtil.relativeToTimeRange(relativeTimeRange); 242 const { interval, intervalMs } = getIntervals(range, minInterval ?? datasource.interval, maxDataPoints); 243 const queryVariables = { 244 __interval: { text: interval, value: interval }, 245 __interval_ms: { text: intervalMs, value: intervalMs }, 246 ...scopedVars, 247 }; 248 249 const interpolatedTarget = datasource.interpolateVariablesInQueries 250 ? await datasource.interpolateVariablesInQueries([target], queryVariables)[0] 251 : target; 252 253 // expressions 254 if (dsRef.uid === ExpressionDatasourceUID) { 255 const newQuery: AlertQuery = { 256 refId: interpolatedTarget.refId, 257 queryType: '', 258 relativeTimeRange, 259 datasourceUid: ExpressionDatasourceUID, 260 model: interpolatedTarget, 261 }; 262 result.push(newQuery); 263 // queries 264 } else { 265 const datasourceSettings = getDataSourceSrv().getInstanceSettings(dsRef); 266 if (datasourceSettings && datasourceSettings.meta.alerting) { 267 const newQuery: AlertQuery = { 268 refId: interpolatedTarget.refId, 269 queryType: interpolatedTarget.queryType ?? '', 270 relativeTimeRange, 271 datasourceUid: datasourceSettings.uid, 272 model: { 273 ...interpolatedTarget, 274 maxDataPoints, 275 intervalMs, 276 }, 277 }; 278 result.push(newQuery); 279 } 280 } 281 } 282 return result; 283}; 284 285export const panelToRuleFormValues = async ( 286 panel: PanelModel, 287 dashboard: DashboardModel 288): Promise<Partial<RuleFormValues> | undefined> => { 289 const { targets } = panel; 290 if (!panel.id || !dashboard.uid) { 291 return undefined; 292 } 293 294 const relativeTimeRange = rangeUtil.timeRangeToRelative(rangeUtil.convertRawToRange(dashboard.time)); 295 const queries = await dataQueriesToGrafanaQueries( 296 targets, 297 relativeTimeRange, 298 panel.scopedVars || {}, 299 panel.datasource ?? undefined, 300 panel.maxDataPoints ?? undefined, 301 panel.interval ?? undefined 302 ); 303 // if no alerting capable queries are found, can't create a rule 304 if (!queries.length || !queries.find((query) => query.datasourceUid !== ExpressionDatasourceUID)) { 305 return undefined; 306 } 307 308 if (!queries.find((query) => query.datasourceUid === ExpressionDatasourceUID)) { 309 queries.push(getDefaultExpression(getNextRefIdChar(queries.map((query) => query.model)))); 310 } 311 312 const { folderId, folderTitle } = dashboard.meta; 313 314 const formValues = { 315 type: RuleFormType.grafana, 316 folder: 317 folderId && folderTitle 318 ? { 319 id: folderId, 320 title: folderTitle, 321 } 322 : undefined, 323 queries, 324 name: panel.title, 325 condition: queries[queries.length - 1].refId, 326 annotations: [ 327 { 328 key: Annotation.dashboardUID, 329 value: dashboard.uid, 330 }, 331 { 332 key: Annotation.panelID, 333 value: String(panel.id), 334 }, 335 ], 336 }; 337 return formValues; 338}; 339 340export function getIntervals(range: TimeRange, lowLimit?: string, resolution?: number): IntervalValues { 341 if (!resolution) { 342 if (lowLimit && rangeUtil.intervalToMs(lowLimit) > 1000) { 343 return { 344 interval: lowLimit, 345 intervalMs: rangeUtil.intervalToMs(lowLimit), 346 }; 347 } 348 return { interval: '1s', intervalMs: 1000 }; 349 } 350 351 return rangeUtil.calculateInterval(range, resolution, lowLimit); 352} 353