Skip to content

Commit c21bb91

Browse files
waleedlatif1claude
andauthored
improvement(grafana): align tools and block with Grafana API spec (#4574)
* improvement(grafana): align tools and block with official Grafana API spec Validates and corrects the Grafana integration against the official API docs: fixes wire-format field naming for provisioned alert rules (missing_series_evals_to_resolve, keepFiringFor, orgID), adds X-Disable-Provenance support, expands alert-rule params (isPaused, notificationSettings, record, annotations, labels), corrects defaults (execErrState=Error, dashboard overwrite=false), and centralizes alert-rule output mapping in a shared utils module. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(grafana): correct wire-format casing for provisioned alert rule fields Grafana's ProvisionedAlertRule schema (verified against upstream Go source and swagger spec) uses keep_firing_for (snake_case) and missingSeriesEvalsToResolve (camelCase) — the opposite of what prior audit rounds assumed. POST/PUT bodies now send the correct field names; mapAlertRule reads the correct primary names with the old casings kept as fallbacks. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(grafana): address PR review feedback - Drop hardcoded orgID: 1 fallback; only send orgID when organizationId is provided, so token-scoped org context drives rule placement. - Surface invalid JSON for notificationSettings/record on alert rule create/update instead of silently dropping the input. - Fix execErrState description in update_alert_rule to include Error. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(grafana): surface invalid JSON for annotations/labels/data on alert rules Match the behavior of other JSON params (data, notificationSettings, record): return a descriptive error instead of silently falling back to {} (create) or keeping the existing value (update). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(grafana): expose alert-rule output fields in generated docs Move ALERT_RULE_OUTPUT_FIELDS from utils.ts to types.ts and rename to SCREAMING_SNAKE_CASE so scripts/generate-docs.ts (which only resolves const references from types.ts matching [A-Z][A-Z_0-9]+) can inline the per-field rows into the generated alert-rule output tables. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 773cd84 commit c21bb91

18 files changed

Lines changed: 1098 additions & 521 deletions

apps/docs/content/docs/en/tools/grafana.mdx

Lines changed: 131 additions & 26 deletions
Large diffs are not rendered by default.

apps/sim/blocks/blocks/grafana.ts

Lines changed: 253 additions & 23 deletions
Large diffs are not rendered by default.

apps/sim/tools/grafana/create_alert_rule.ts

Lines changed: 85 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type {
22
GrafanaCreateAlertRuleParams,
33
GrafanaCreateAlertRuleResponse,
44
} from '@/tools/grafana/types'
5+
import { ALERT_RULE_OUTPUT_FIELDS } from '@/tools/grafana/types'
6+
import { mapAlertRule } from '@/tools/grafana/utils'
57
import type { ToolConfig } from '@/tools/types'
68

79
export const createAlertRuleTool: ToolConfig<
@@ -52,9 +54,10 @@ export const createAlertRuleTool: ToolConfig<
5254
},
5355
condition: {
5456
type: 'string',
55-
required: true,
57+
required: false,
5658
visibility: 'user-or-llm',
57-
description: 'The refId of the query or expression to use as the alert condition',
59+
description:
60+
'The refId of the query or expression to use as the alert condition (required for alerting rules; omit for recording rules)',
5861
},
5962
data: {
6063
type: 'string',
@@ -78,7 +81,7 @@ export const createAlertRuleTool: ToolConfig<
7881
type: 'string',
7982
required: false,
8083
visibility: 'user-only',
81-
description: 'State on execution error (Alerting, OK)',
84+
description: 'State on execution error (Error, Alerting, OK)',
8285
},
8386
annotations: {
8487
type: 'string',
@@ -92,6 +95,48 @@ export const createAlertRuleTool: ToolConfig<
9295
visibility: 'user-or-llm',
9396
description: 'JSON object of labels',
9497
},
98+
uid: {
99+
type: 'string',
100+
required: false,
101+
visibility: 'user-or-llm',
102+
description: 'Optional custom UID for the alert rule',
103+
},
104+
isPaused: {
105+
type: 'boolean',
106+
required: false,
107+
visibility: 'user-only',
108+
description: 'Whether the rule is paused on creation',
109+
},
110+
keepFiringFor: {
111+
type: 'string',
112+
required: false,
113+
visibility: 'user-or-llm',
114+
description: 'Duration to keep firing after the condition stops (e.g., 5m)',
115+
},
116+
missingSeriesEvalsToResolve: {
117+
type: 'number',
118+
required: false,
119+
visibility: 'user-only',
120+
description: 'Number of missing series evaluations before resolving',
121+
},
122+
notificationSettings: {
123+
type: 'string',
124+
required: false,
125+
visibility: 'user-only',
126+
description: 'JSON object of per-rule notification settings (overrides)',
127+
},
128+
record: {
129+
type: 'string',
130+
required: false,
131+
visibility: 'user-or-llm',
132+
description: 'JSON object configuring this as a recording rule (omit for alerting rules)',
133+
},
134+
disableProvenance: {
135+
type: 'boolean',
136+
required: false,
137+
visibility: 'user-only',
138+
description: 'Set X-Disable-Provenance header so the rule remains editable in the Grafana UI',
139+
},
95140
},
96141

97142
request: {
@@ -105,40 +150,67 @@ export const createAlertRuleTool: ToolConfig<
105150
if (params.organizationId) {
106151
headers['X-Grafana-Org-Id'] = params.organizationId
107152
}
153+
if (params.disableProvenance) {
154+
headers['X-Disable-Provenance'] = 'true'
155+
}
108156
return headers
109157
},
110158
body: (params) => {
111-
let dataArray: any[] = []
159+
let dataArray: unknown[] = []
112160
try {
113161
dataArray = JSON.parse(params.data)
114162
} catch {
115163
throw new Error('Invalid JSON for data parameter')
116164
}
117165

118-
const body: Record<string, any> = {
166+
const body: Record<string, unknown> = {
119167
title: params.title,
120168
folderUID: params.folderUid,
121169
ruleGroup: params.ruleGroup,
122-
condition: params.condition,
123170
data: dataArray,
124-
for: params.forDuration || '5m',
125-
noDataState: params.noDataState || 'NoData',
126-
execErrState: params.execErrState || 'Alerting',
171+
}
172+
if (params.organizationId) body.orgID = Number(params.organizationId)
173+
174+
if (params.condition) body.condition = params.condition
175+
if (params.uid) body.uid = params.uid
176+
if (params.forDuration) body.for = params.forDuration
177+
if (params.noDataState) body.noDataState = params.noDataState
178+
if (params.execErrState) body.execErrState = params.execErrState
179+
if (params.isPaused !== undefined) body.isPaused = params.isPaused
180+
if (params.keepFiringFor) body.keep_firing_for = params.keepFiringFor
181+
if (params.missingSeriesEvalsToResolve !== undefined) {
182+
body.missingSeriesEvalsToResolve = params.missingSeriesEvalsToResolve
127183
}
128184

129185
if (params.annotations) {
130186
try {
131187
body.annotations = JSON.parse(params.annotations)
132188
} catch {
133-
body.annotations = {}
189+
throw new Error('Invalid JSON for annotations parameter')
134190
}
135191
}
136192

137193
if (params.labels) {
138194
try {
139195
body.labels = JSON.parse(params.labels)
140196
} catch {
141-
body.labels = {}
197+
throw new Error('Invalid JSON for labels parameter')
198+
}
199+
}
200+
201+
if (params.notificationSettings) {
202+
try {
203+
body.notification_settings = JSON.parse(params.notificationSettings)
204+
} catch {
205+
throw new Error('Invalid JSON for notificationSettings parameter')
206+
}
207+
}
208+
209+
if (params.record) {
210+
try {
211+
body.record = JSON.parse(params.record)
212+
} catch {
213+
throw new Error('Invalid JSON for record parameter')
142214
}
143215
}
144216

@@ -148,47 +220,8 @@ export const createAlertRuleTool: ToolConfig<
148220

149221
transformResponse: async (response: Response) => {
150222
const data = await response.json()
151-
152-
return {
153-
success: true,
154-
output: {
155-
uid: data.uid,
156-
title: data.title,
157-
condition: data.condition,
158-
data: data.data,
159-
updated: data.updated,
160-
noDataState: data.noDataState,
161-
execErrState: data.execErrState,
162-
for: data.for,
163-
annotations: data.annotations || {},
164-
labels: data.labels || {},
165-
isPaused: data.isPaused || false,
166-
folderUID: data.folderUID,
167-
ruleGroup: data.ruleGroup,
168-
orgId: data.orgId,
169-
namespace_uid: data.namespace_uid,
170-
namespace_id: data.namespace_id,
171-
provenance: data.provenance || '',
172-
},
173-
}
223+
return { success: true, output: mapAlertRule(data) }
174224
},
175225

176-
outputs: {
177-
uid: {
178-
type: 'string',
179-
description: 'The UID of the created alert rule',
180-
},
181-
title: {
182-
type: 'string',
183-
description: 'Alert rule title',
184-
},
185-
folderUID: {
186-
type: 'string',
187-
description: 'Parent folder UID',
188-
},
189-
ruleGroup: {
190-
type: 'string',
191-
description: 'Rule group name',
192-
},
193-
},
226+
outputs: ALERT_RULE_OUTPUT_FIELDS,
194227
}

apps/sim/tools/grafana/create_annotation.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ export const createAnnotationTool: ToolConfig<
4646
},
4747
dashboardUid: {
4848
type: 'string',
49-
required: true,
49+
required: false,
5050
visibility: 'user-or-llm',
51-
description: 'UID of the dashboard to add the annotation to (e.g., abc123def)',
51+
description:
52+
'UID of the dashboard to add the annotation to (e.g., abc123def). Omit to create a global organization annotation.',
5253
},
5354
panelId: {
5455
type: 'number',
@@ -84,30 +85,22 @@ export const createAnnotationTool: ToolConfig<
8485
return headers
8586
},
8687
body: (params) => {
87-
const body: Record<string, any> = {
88+
const body: Record<string, unknown> = {
8889
text: params.text,
89-
time: params.time || Date.now(),
9090
}
9191

92+
if (params.time) body.time = params.time
93+
if (params.timeEnd) body.timeEnd = params.timeEnd
94+
if (params.dashboardUid) body.dashboardUID = params.dashboardUid
95+
if (params.panelId) body.panelId = params.panelId
96+
9297
if (params.tags) {
9398
body.tags = params.tags
9499
.split(',')
95100
.map((t) => t.trim())
96101
.filter((t) => t)
97102
}
98103

99-
if (params.dashboardUid) {
100-
body.dashboardUID = params.dashboardUid
101-
}
102-
103-
if (params.panelId) {
104-
body.panelId = params.panelId
105-
}
106-
107-
if (params.timeEnd) {
108-
body.timeEnd = params.timeEnd
109-
}
110-
111104
return body
112105
},
113106
},

0 commit comments

Comments
 (0)