alertmanager.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import { SelectableValue } from '@grafana/data';
  2. import { FetchError } from '@grafana/runtime';
  3. import {
  4. AlertManagerCortexConfig,
  5. MatcherOperator,
  6. Route,
  7. Matcher,
  8. TimeInterval,
  9. TimeRange,
  10. } from 'app/plugins/datasource/alertmanager/types';
  11. import { Labels } from 'app/types/unified-alerting-dto';
  12. import { MatcherFieldValue } from '../types/silence-form';
  13. import { getAllDataSources } from './config';
  14. import { DataSourceType } from './datasource';
  15. export function addDefaultsToAlertmanagerConfig(config: AlertManagerCortexConfig): AlertManagerCortexConfig {
  16. // add default receiver if it does not exist
  17. if (!config.alertmanager_config.receivers) {
  18. config.alertmanager_config.receivers = [{ name: 'default ' }];
  19. }
  20. // add default route if it does not exists
  21. if (!config.alertmanager_config.route) {
  22. config.alertmanager_config.route = {
  23. receiver: config.alertmanager_config.receivers![0].name,
  24. };
  25. }
  26. if (!config.template_files) {
  27. config.template_files = {};
  28. }
  29. return config;
  30. }
  31. export function removeMuteTimingFromRoute(muteTiming: string, route: Route): Route {
  32. const newRoute: Route = {
  33. ...route,
  34. mute_time_intervals: route.mute_time_intervals?.filter((muteName) => muteName !== muteTiming) ?? [],
  35. routes: route.routes?.map((subRoute) => removeMuteTimingFromRoute(muteTiming, subRoute)),
  36. };
  37. return newRoute;
  38. }
  39. export function renameMuteTimings(newMuteTimingName: string, oldMuteTimingName: string, route: Route): Route {
  40. return {
  41. ...route,
  42. mute_time_intervals: route.mute_time_intervals?.map((name) =>
  43. name === oldMuteTimingName ? newMuteTimingName : name
  44. ),
  45. routes: route.routes?.map((subRoute) => renameMuteTimings(newMuteTimingName, oldMuteTimingName, subRoute)),
  46. };
  47. }
  48. function isReceiverUsedInRoute(receiver: string, route: Route): boolean {
  49. return (
  50. (route.receiver === receiver || route.routes?.some((route) => isReceiverUsedInRoute(receiver, route))) ?? false
  51. );
  52. }
  53. export function isReceiverUsed(receiver: string, config: AlertManagerCortexConfig): boolean {
  54. return (
  55. (config.alertmanager_config.route && isReceiverUsedInRoute(receiver, config.alertmanager_config.route)) ?? false
  56. );
  57. }
  58. export function matcherToOperator(matcher: Matcher): MatcherOperator {
  59. if (matcher.isEqual) {
  60. if (matcher.isRegex) {
  61. return MatcherOperator.regex;
  62. } else {
  63. return MatcherOperator.equal;
  64. }
  65. } else if (matcher.isRegex) {
  66. return MatcherOperator.notRegex;
  67. } else {
  68. return MatcherOperator.notEqual;
  69. }
  70. }
  71. export function matcherOperatorToValue(operator: MatcherOperator) {
  72. switch (operator) {
  73. case MatcherOperator.equal:
  74. return { isEqual: true, isRegex: false };
  75. case MatcherOperator.notEqual:
  76. return { isEqual: false, isRegex: false };
  77. case MatcherOperator.regex:
  78. return { isEqual: true, isRegex: true };
  79. case MatcherOperator.notRegex:
  80. return { isEqual: false, isRegex: true };
  81. }
  82. }
  83. export function matcherToMatcherField(matcher: Matcher): MatcherFieldValue {
  84. return {
  85. name: matcher.name,
  86. value: matcher.value,
  87. operator: matcherToOperator(matcher),
  88. };
  89. }
  90. export function matcherFieldToMatcher(field: MatcherFieldValue): Matcher {
  91. return {
  92. name: field.name,
  93. value: field.value,
  94. ...matcherOperatorToValue(field.operator),
  95. };
  96. }
  97. export function matchersToString(matchers: Matcher[]) {
  98. const matcherFields = matchers.map(matcherToMatcherField);
  99. const combinedMatchers = matcherFields.reduce((acc, current) => {
  100. const currentMatcherString = `${current.name}${current.operator}"${current.value}"`;
  101. return acc ? `${acc},${currentMatcherString}` : currentMatcherString;
  102. }, '');
  103. return `{${combinedMatchers}}`;
  104. }
  105. export const matcherFieldOptions: SelectableValue[] = [
  106. { label: MatcherOperator.equal, description: 'Equals', value: MatcherOperator.equal },
  107. { label: MatcherOperator.notEqual, description: 'Does not equal', value: MatcherOperator.notEqual },
  108. { label: MatcherOperator.regex, description: 'Matches regex', value: MatcherOperator.regex },
  109. { label: MatcherOperator.notRegex, description: 'Does not match regex', value: MatcherOperator.notRegex },
  110. ];
  111. const matcherOperators = [
  112. MatcherOperator.regex,
  113. MatcherOperator.notRegex,
  114. MatcherOperator.notEqual,
  115. MatcherOperator.equal,
  116. ];
  117. export function parseMatcher(matcher: string): Matcher {
  118. const trimmed = matcher.trim();
  119. if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
  120. throw new Error(`PromQL matchers not supported yet, sorry! PromQL matcher found: ${trimmed}`);
  121. }
  122. const operatorsFound = matcherOperators
  123. .map((op): [MatcherOperator, number] => [op, trimmed.indexOf(op)])
  124. .filter(([_, idx]) => idx > -1)
  125. .sort((a, b) => a[1] - b[1]);
  126. if (!operatorsFound.length) {
  127. throw new Error(`Invalid matcher: ${trimmed}`);
  128. }
  129. const [operator, idx] = operatorsFound[0];
  130. const name = trimmed.slice(0, idx).trim();
  131. const value = trimmed.slice(idx + operator.length).trim();
  132. if (!name) {
  133. throw new Error(`Invalid matcher: ${trimmed}`);
  134. }
  135. return {
  136. name,
  137. value,
  138. isRegex: operator === MatcherOperator.regex || operator === MatcherOperator.notRegex,
  139. isEqual: operator === MatcherOperator.equal || operator === MatcherOperator.regex,
  140. };
  141. }
  142. export function parseMatchers(matcherQueryString: string): Matcher[] {
  143. const matcherRegExp = /\b([\w.-]+)(=~|!=|!~|=(?="?\w))"?([^"\n,]*)"?/g;
  144. const matchers: Matcher[] = [];
  145. matcherQueryString.replace(matcherRegExp, (_, key, operator, value) => {
  146. const isEqual = operator === MatcherOperator.equal || operator === MatcherOperator.regex;
  147. const isRegex = operator === MatcherOperator.regex || operator === MatcherOperator.notRegex;
  148. matchers.push({
  149. name: key,
  150. value,
  151. isEqual,
  152. isRegex,
  153. });
  154. return '';
  155. });
  156. return matchers;
  157. }
  158. export function labelsMatchMatchers(labels: Labels, matchers: Matcher[]): boolean {
  159. return matchers.every(({ name, value, isRegex, isEqual }) => {
  160. return Object.entries(labels).some(([labelKey, labelValue]) => {
  161. const nameMatches = name === labelKey;
  162. let valueMatches;
  163. if (isEqual && !isRegex) {
  164. valueMatches = value === labelValue;
  165. }
  166. if (!isEqual && !isRegex) {
  167. valueMatches = value !== labelValue;
  168. }
  169. if (isEqual && isRegex) {
  170. valueMatches = new RegExp(value).test(labelValue);
  171. }
  172. if (!isEqual && isRegex) {
  173. valueMatches = !new RegExp(value).test(labelValue);
  174. }
  175. return nameMatches && valueMatches;
  176. });
  177. });
  178. }
  179. export function getAllAlertmanagerDataSources() {
  180. return getAllDataSources().filter((ds) => ds.type === DataSourceType.Alertmanager);
  181. }
  182. export function getAlertmanagerByUid(uid?: string) {
  183. return getAllAlertmanagerDataSources().find((ds) => uid === ds.uid);
  184. }
  185. export function timeIntervalToString(timeInterval: TimeInterval): string {
  186. const { times, weekdays, days_of_month, months, years } = timeInterval;
  187. const timeString = getTimeString(times);
  188. const weekdayString = getWeekdayString(weekdays);
  189. const daysString = getDaysOfMonthString(days_of_month);
  190. const monthsString = getMonthsString(months);
  191. const yearsString = getYearsString(years);
  192. return [timeString, weekdayString, daysString, monthsString, yearsString].join(', ');
  193. }
  194. export function getTimeString(times?: TimeRange[]): string {
  195. return (
  196. 'Times: ' +
  197. (times ? times?.map(({ start_time, end_time }) => `${start_time} - ${end_time} UTC`).join(' and ') : 'All')
  198. );
  199. }
  200. export function getWeekdayString(weekdays?: string[]): string {
  201. return (
  202. 'Weekdays: ' +
  203. (weekdays
  204. ?.map((day) => {
  205. if (day.includes(':')) {
  206. return day
  207. .split(':')
  208. .map((d) => {
  209. const abbreviated = d.slice(0, 3);
  210. return abbreviated[0].toLocaleUpperCase() + abbreviated.slice(1);
  211. })
  212. .join('-');
  213. } else {
  214. const abbreviated = day.slice(0, 3);
  215. return abbreviated[0].toLocaleUpperCase() + abbreviated.slice(1);
  216. }
  217. })
  218. .join(', ') ?? 'All')
  219. );
  220. }
  221. export function getDaysOfMonthString(daysOfMonth?: string[]): string {
  222. return 'Days of the month: ' + (daysOfMonth?.join(', ') ?? 'All');
  223. }
  224. export function getMonthsString(months?: string[]): string {
  225. return 'Months: ' + (months?.join(', ') ?? 'All');
  226. }
  227. export function getYearsString(years?: string[]): string {
  228. return 'Years: ' + (years?.join(', ') ?? 'All');
  229. }
  230. export function isFetchError(e: unknown): e is FetchError {
  231. return typeof e === 'object' && e !== null && 'status' in e && 'data' in e;
  232. }