ruler.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import { lastValueFrom } from 'rxjs';
  2. import { FetchResponse, getBackendSrv } from '@grafana/runtime';
  3. import { RulerDataSourceConfig } from 'app/types/unified-alerting';
  4. import { PostableRulerRuleGroupDTO, RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
  5. import { RULER_NOT_SUPPORTED_MSG } from '../utils/constants';
  6. import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
  7. import { prepareRulesFilterQueryParams } from './prometheus';
  8. interface ErrorResponseMessage {
  9. message?: string;
  10. error?: string;
  11. }
  12. export interface RulerRequestUrl {
  13. path: string;
  14. params?: Record<string, string>;
  15. }
  16. export function rulerUrlBuilder(rulerConfig: RulerDataSourceConfig) {
  17. const grafanaServerPath = `/api/ruler/${getDatasourceAPIUid(rulerConfig.dataSourceName)}`;
  18. const rulerPath = `${grafanaServerPath}/api/v1/rules`;
  19. const rulerSearchParams = new URLSearchParams();
  20. rulerSearchParams.set('subtype', rulerConfig.apiVersion === 'legacy' ? 'cortex' : 'mimir');
  21. return {
  22. rules: (filter?: FetchRulerRulesFilter): RulerRequestUrl => {
  23. const params = prepareRulesFilterQueryParams(rulerSearchParams, filter);
  24. return {
  25. path: `${rulerPath}`,
  26. params: params,
  27. };
  28. },
  29. namespace: (namespace: string): RulerRequestUrl => ({
  30. path: `${rulerPath}/${encodeURIComponent(namespace)}`,
  31. params: Object.fromEntries(rulerSearchParams),
  32. }),
  33. namespaceGroup: (namespace: string, group: string): RulerRequestUrl => ({
  34. path: `${rulerPath}/${encodeURIComponent(namespace)}/${encodeURIComponent(group)}`,
  35. params: Object.fromEntries(rulerSearchParams),
  36. }),
  37. };
  38. }
  39. // upsert a rule group. use this to update rule
  40. export async function setRulerRuleGroup(
  41. rulerConfig: RulerDataSourceConfig,
  42. namespace: string,
  43. group: PostableRulerRuleGroupDTO
  44. ): Promise<void> {
  45. const { path, params } = rulerUrlBuilder(rulerConfig).namespace(namespace);
  46. await lastValueFrom(
  47. getBackendSrv().fetch<unknown>({
  48. method: 'POST',
  49. url: path,
  50. data: group,
  51. showErrorAlert: false,
  52. showSuccessAlert: false,
  53. params,
  54. })
  55. );
  56. }
  57. export interface FetchRulerRulesFilter {
  58. dashboardUID: string;
  59. panelId?: number;
  60. }
  61. // fetch all ruler rule namespaces and included groups
  62. export async function fetchRulerRules(rulerConfig: RulerDataSourceConfig, filter?: FetchRulerRulesFilter) {
  63. if (filter?.dashboardUID && rulerConfig.dataSourceName !== GRAFANA_RULES_SOURCE_NAME) {
  64. throw new Error('Filtering by dashboard UID is only supported by Grafana.');
  65. }
  66. // TODO Move params creation to the rules function
  67. const { path: url, params } = rulerUrlBuilder(rulerConfig).rules(filter);
  68. return rulerGetRequest<RulerRulesConfigDTO>(url, {}, params);
  69. }
  70. // fetch rule groups for a particular namespace
  71. // will throw with { status: 404 } if namespace does not exist
  72. export async function fetchRulerRulesNamespace(rulerConfig: RulerDataSourceConfig, namespace: string) {
  73. const { path, params } = rulerUrlBuilder(rulerConfig).namespace(namespace);
  74. const result = await rulerGetRequest<Record<string, RulerRuleGroupDTO[]>>(path, {}, params);
  75. return result[namespace] || [];
  76. }
  77. // fetch a particular rule group
  78. // will throw with { status: 404 } if rule group does not exist
  79. export async function fetchTestRulerRulesGroup(dataSourceName: string): Promise<RulerRuleGroupDTO | null> {
  80. return rulerGetRequest<RulerRuleGroupDTO | null>(
  81. `/api/ruler/${getDatasourceAPIUid(dataSourceName)}/api/v1/rules/test/test`,
  82. null
  83. );
  84. }
  85. export async function fetchRulerRulesGroup(
  86. rulerConfig: RulerDataSourceConfig,
  87. namespace: string,
  88. group: string
  89. ): Promise<RulerRuleGroupDTO | null> {
  90. const { path, params } = rulerUrlBuilder(rulerConfig).namespaceGroup(namespace, group);
  91. return rulerGetRequest<RulerRuleGroupDTO | null>(path, null, params);
  92. }
  93. export async function deleteRulerRulesGroup(rulerConfig: RulerDataSourceConfig, namespace: string, groupName: string) {
  94. const { path, params } = rulerUrlBuilder(rulerConfig).namespaceGroup(namespace, groupName);
  95. await lastValueFrom(
  96. getBackendSrv().fetch({
  97. url: path,
  98. method: 'DELETE',
  99. showSuccessAlert: false,
  100. showErrorAlert: false,
  101. params,
  102. })
  103. );
  104. }
  105. // false in case ruler is not supported. this is weird, but we'll work on it
  106. async function rulerGetRequest<T>(url: string, empty: T, params?: Record<string, string>): Promise<T> {
  107. try {
  108. const response = await lastValueFrom(
  109. getBackendSrv().fetch<T>({
  110. url,
  111. showErrorAlert: false,
  112. showSuccessAlert: false,
  113. params,
  114. })
  115. );
  116. return response.data;
  117. } catch (error) {
  118. if (!isResponseError(error)) {
  119. throw error;
  120. }
  121. if (isCortexErrorResponse(error)) {
  122. return empty;
  123. } else if (isRulerNotSupported(error)) {
  124. // assert if the endoint is not supported at all
  125. throw {
  126. ...error,
  127. data: {
  128. ...error.data,
  129. message: RULER_NOT_SUPPORTED_MSG,
  130. },
  131. };
  132. }
  133. throw error;
  134. }
  135. }
  136. function isResponseError(error: unknown): error is FetchResponse<ErrorResponseMessage> {
  137. const hasErrorMessage = (error as FetchResponse<ErrorResponseMessage>).data != null;
  138. const hasErrorCode = Number.isFinite((error as FetchResponse<ErrorResponseMessage>).status);
  139. return hasErrorCode && hasErrorMessage;
  140. }
  141. function isRulerNotSupported(error: FetchResponse<ErrorResponseMessage>) {
  142. return (
  143. error.status === 404 ||
  144. (error.status === 500 &&
  145. error.data.message?.includes('unexpected content type from upstream. expected YAML, got text/html'))
  146. );
  147. }
  148. function isCortexErrorResponse(error: FetchResponse<ErrorResponseMessage>) {
  149. return (
  150. error.status === 404 &&
  151. (error.data.message?.includes('group does not exist') || error.data.message?.includes('no rule groups found'))
  152. );
  153. }
  154. export async function deleteNamespace(rulerConfig: RulerDataSourceConfig, namespace: string): Promise<void> {
  155. const { path, params } = rulerUrlBuilder(rulerConfig).namespace(namespace);
  156. await lastValueFrom(
  157. getBackendSrv().fetch<unknown>({
  158. method: 'DELETE',
  159. url: path,
  160. showErrorAlert: false,
  161. showSuccessAlert: false,
  162. params,
  163. })
  164. );
  165. }