Skip to content

Commit 6fe9de0

Browse files
authored
feat(tools): support optional projectName for Cloud Monitoring datasources (#710)
Co-authored-by: Ilia Lazebnik <Ilia.lazebnik@gmail.com>
1 parent 81e8543 commit 6fe9de0

4 files changed

Lines changed: 64 additions & 29 deletions

File tree

tools/prom_backend.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,22 @@ type promBackend interface {
3333
}
3434

3535
// backendForDatasource looks up the datasource type and returns the appropriate backend.
36-
func backendForDatasource(ctx context.Context, uid string) (promBackend, error) {
36+
// An optional projectOverride can be passed for Cloud Monitoring datasources to override
37+
// (or substitute for) the defaultProject configured on the datasource.
38+
func backendForDatasource(ctx context.Context, uid string, projectOverride ...string) (promBackend, error) {
3739
ds, err := getDatasourceByUID(ctx, GetDatasourceByUIDParams{UID: uid})
3840
if err != nil {
3941
return nil, err
4042
}
4143

44+
proj := ""
45+
if len(projectOverride) > 0 {
46+
proj = projectOverride[0]
47+
}
48+
4249
switch ds.Type {
4350
case "stackdriver":
44-
return newCloudMonitoringBackend(ctx, ds)
51+
return newCloudMonitoringBackend(ctx, ds, proj)
4552
default:
4653
// For prometheus, thanos, cortex, mimir, and any other Prometheus-compatible datasource,
4754
// use the native Prometheus client via the datasource proxy.

tools/prom_backend_cloudmonitoring.go

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ type cloudMonitoringBackend struct {
2929
defaultProject string
3030
}
3131

32-
func newCloudMonitoringBackend(ctx context.Context, ds *models.DataSource) (*cloudMonitoringBackend, error) {
33-
defaultProject, err := extractCMDefaultProject(ds)
34-
if err != nil {
35-
return nil, err
32+
func newCloudMonitoringBackend(ctx context.Context, ds *models.DataSource, projectOverride string) (*cloudMonitoringBackend, error) {
33+
defaultProject := extractCMDefaultProject(ds)
34+
if projectOverride != "" {
35+
defaultProject = projectOverride
3636
}
3737

3838
cfg := mcpgrafana.GrafanaConfigFromContext(ctx)
@@ -59,23 +59,33 @@ func newCloudMonitoringBackend(ctx context.Context, ds *models.DataSource) (*clo
5959
}, nil
6060
}
6161

62-
func extractCMDefaultProject(ds *models.DataSource) (string, error) {
62+
func extractCMDefaultProject(ds *models.DataSource) string {
6363
if ds.JSONData == nil {
64-
return "", fmt.Errorf("cloud monitoring datasource %s has no jsonData configured", ds.UID)
64+
return ""
6565
}
6666
jsonDataMap, ok := ds.JSONData.(map[string]interface{})
6767
if !ok {
68-
return "", fmt.Errorf("cloud monitoring datasource %s has unexpected jsonData format", ds.UID)
68+
return ""
6969
}
70-
proj, ok := jsonDataMap["defaultProject"].(string)
71-
if !ok || proj == "" {
72-
return "", fmt.Errorf("cloud monitoring datasource %s has no defaultProject configured in jsonData", ds.UID)
70+
proj, _ := jsonDataMap["defaultProject"].(string)
71+
return proj
72+
}
73+
74+
// project returns the configured GCP project or an actionable error if neither
75+
// defaultProject nor a per-call projectName override was supplied.
76+
func (b *cloudMonitoringBackend) project() (string, error) {
77+
if b.defaultProject == "" {
78+
return "", fmt.Errorf("no GCP project configured: set defaultProject on the datasource or pass projectName in the tool call")
7379
}
74-
return proj, nil
80+
return b.defaultProject, nil
7581
}
7682

7783
// Query executes a PromQL query via Grafana's /api/ds/query endpoint.
7884
func (b *cloudMonitoringBackend) Query(ctx context.Context, expr string, queryType string, start, end time.Time, stepSeconds int) (model.Value, error) {
85+
project, err := b.project()
86+
if err != nil {
87+
return nil, err
88+
}
7989
step := fmt.Sprintf("%ds", stepSeconds)
8090
if stepSeconds == 0 {
8191
step = "60s"
@@ -99,14 +109,14 @@ func (b *cloudMonitoringBackend) Query(ctx context.Context, expr string, queryTy
99109
"queryType": "promQL",
100110
"promQLQuery": map[string]interface{}{
101111
"expr": expr,
102-
"projectName": b.defaultProject,
112+
"projectName": project,
103113
"step": step,
104114
},
105115
// timeSeriesList is required by the Cloud Monitoring plugin even for
106116
// PromQL queries — without it the plugin returns a 500 error.
107117
"timeSeriesList": map[string]interface{}{
108118
"filters": []interface{}{},
109-
"projectName": b.defaultProject,
119+
"projectName": project,
110120
"view": "FULL",
111121
},
112122
}
@@ -130,6 +140,10 @@ func (b *cloudMonitoringBackend) Query(ctx context.Context, expr string, queryTy
130140
// The Grafana Cloud Monitoring plugin strips label descriptors from metric
131141
// descriptors, so we must query actual time series to discover labels.
132142
func (b *cloudMonitoringBackend) LabelNames(ctx context.Context, matchers []string, start, end time.Time) ([]string, error) {
143+
project, err := b.project()
144+
if err != nil {
145+
return nil, err
146+
}
133147
if start.IsZero() {
134148
start = time.Now().Add(-1 * time.Hour)
135149
}
@@ -153,7 +167,7 @@ func (b *cloudMonitoringBackend) LabelNames(ctx context.Context, matchers []stri
153167
},
154168
"queryType": "timeSeriesList",
155169
"timeSeriesList": map[string]interface{}{
156-
"projectName": b.defaultProject,
170+
"projectName": project,
157171
"filters": filters,
158172
"view": "HEADERS",
159173
"crossSeriesReducer": "REDUCE_NONE",
@@ -231,6 +245,10 @@ func (b *cloudMonitoringBackend) metricNames(ctx context.Context, matchers []str
231245
// labelValuesViaQuery uses a TIME_SERIES_LIST HEADERS query via /api/ds/query
232246
// to discover label values, matching how the Grafana frontend does it.
233247
func (b *cloudMonitoringBackend) labelValuesViaQuery(ctx context.Context, labelName string, matchers []string, start, end time.Time) ([]string, error) {
248+
project, err := b.project()
249+
if err != nil {
250+
return nil, err
251+
}
234252
if start.IsZero() {
235253
start = time.Now().Add(-1 * time.Hour)
236254
}
@@ -254,7 +272,7 @@ func (b *cloudMonitoringBackend) labelValuesViaQuery(ctx context.Context, labelN
254272
},
255273
"queryType": "timeSeriesList",
256274
"timeSeriesList": map[string]interface{}{
257-
"projectName": b.defaultProject,
275+
"projectName": project,
258276
"filters": filters,
259277
"view": "HEADERS",
260278
"crossSeriesReducer": "REDUCE_NONE",
@@ -315,8 +333,12 @@ func (b *cloudMonitoringBackend) doDSQuery(ctx context.Context, payload map[stri
315333
// The Grafana plugin handles GCP API pagination internally and returns all descriptors
316334
// as a flat JSON array (not the raw GCP API wrapper).
317335
func (b *cloudMonitoringBackend) fetchMetricDescriptors(ctx context.Context) ([]gcpMetricDescriptor, error) {
336+
project, err := b.project()
337+
if err != nil {
338+
return nil, err
339+
}
318340
url := fmt.Sprintf("%s/api/datasources/uid/%s/resources/metricDescriptors/v3/projects/%s/metricDescriptors",
319-
b.baseURL, b.datasourceUID, b.defaultProject)
341+
b.baseURL, b.datasourceUID, project)
320342

321343
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
322344
if err != nil {

tools/prom_backend_cloudmonitoring_test.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -285,23 +285,22 @@ func TestExtractCMDefaultProject(t *testing.T) {
285285
UID: "test",
286286
JSONData: map[string]interface{}{"defaultProject": "my-project"},
287287
}
288-
proj, err := extractCMDefaultProject(ds)
289-
require.NoError(t, err)
288+
proj := extractCMDefaultProject(ds)
290289
assert.Equal(t, "my-project", proj)
291290
})
292291

293292
t.Run("nil jsonData", func(t *testing.T) {
294293
ds := &models.DataSource{UID: "test"}
295-
_, err := extractCMDefaultProject(ds)
296-
require.Error(t, err)
294+
proj := extractCMDefaultProject(ds)
295+
assert.Equal(t, "", proj)
297296
})
298297

299298
t.Run("missing project", func(t *testing.T) {
300299
ds := &models.DataSource{
301300
UID: "test",
302301
JSONData: map[string]interface{}{"somethingElse": "value"},
303302
}
304-
_, err := extractCMDefaultProject(ds)
305-
require.Error(t, err)
303+
proj := extractCMDefaultProject(ds)
304+
assert.Equal(t, "", proj)
306305
})
307306
}

tools/prometheus.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@ type ListPrometheusMetricMetadataParams struct {
3232
Limit int `json:"limit" jsonschema:"default=10,description=The maximum number of metrics to return"`
3333
LimitPerMetric int `json:"limitPerMetric" jsonschema:"description=The maximum number of metrics to return per metric"`
3434
Metric string `json:"metric" jsonschema:"description=The metric to query"`
35+
ProjectName string `json:"projectName,omitempty" jsonschema:"description=GCP project name to query (Cloud Monitoring datasources only). Overrides or substitutes the defaultProject configured on the datasource."`
3536
}
3637

3738
func listPrometheusMetricMetadata(ctx context.Context, args ListPrometheusMetricMetadataParams) (map[string][]promv1.Metadata, error) {
38-
backend, err := backendForDatasource(ctx, args.DatasourceUID)
39+
backend, err := backendForDatasource(ctx, args.DatasourceUID, args.ProjectName)
3940
if err != nil {
4041
return nil, fmt.Errorf("getting backend: %w", err)
4142
}
@@ -68,6 +69,7 @@ type QueryPrometheusParams struct {
6869
EndTime string `json:"endTime" jsonschema:"required,description=The end time. Supported formats are RFC3339 or relative to now (e.g. 'now'\\, 'now-1.5h'\\, 'now-2h45m'). Valid time units are 'ns'\\, 'us' (or 'µs')\\, 'ms'\\, 's'\\, 'm'\\, 'h'\\, 'd'."`
6970
StepSeconds int `json:"stepSeconds,omitempty" jsonschema:"description=The time series step size in seconds. Required if queryType is 'range'\\, ignored if queryType is 'instant'"`
7071
QueryType string `json:"queryType,omitempty" jsonschema:"description=The type of query to use. Either 'range' or 'instant'"`
72+
ProjectName string `json:"projectName,omitempty" jsonschema:"description=GCP project name to query (Cloud Monitoring datasources only). Overrides or substitutes the defaultProject configured on the datasource."`
7173
}
7274

7375
// QueryPrometheusResult wraps the Prometheus query result with optional hints
@@ -106,7 +108,7 @@ func isPrometheusResultEmpty(result model.Value) bool {
106108
// queryPrometheus executes a PromQL query and returns raw results.
107109
// This is the internal function - use queryPrometheusWithHints for MCP tools.
108110
func queryPrometheus(ctx context.Context, args QueryPrometheusParams) (model.Value, error) {
109-
backend, err := backendForDatasource(ctx, args.DatasourceUID)
111+
backend, err := backendForDatasource(ctx, args.DatasourceUID, args.ProjectName)
110112
if err != nil {
111113
return nil, fmt.Errorf("getting backend: %w", err)
112114
}
@@ -178,10 +180,11 @@ type ListPrometheusMetricNamesParams struct {
178180
Regex string `json:"regex" jsonschema:"description=The regex to match against the metric names"`
179181
Limit int `json:"limit,omitempty" jsonschema:"default=10,description=The maximum number of results to return"`
180182
Page int `json:"page,omitempty" jsonschema:"default=1,description=The page number to return"`
183+
ProjectName string `json:"projectName,omitempty" jsonschema:"description=GCP project name to query (Cloud Monitoring datasources only). Overrides or substitutes the defaultProject configured on the datasource."`
181184
}
182185

183186
func listPrometheusMetricNames(ctx context.Context, args ListPrometheusMetricNamesParams) ([]string, error) {
184-
backend, err := backendForDatasource(ctx, args.DatasourceUID)
187+
backend, err := backendForDatasource(ctx, args.DatasourceUID, args.ProjectName)
185188
if err != nil {
186189
return nil, fmt.Errorf("getting backend: %w", err)
187190
}
@@ -294,10 +297,11 @@ type ListPrometheusLabelNamesParams struct {
294297
StartRFC3339 string `json:"startRfc3339,omitempty" jsonschema:"description=Optionally\\, the start time of the time range to filter the results by"`
295298
EndRFC3339 string `json:"endRfc3339,omitempty" jsonschema:"description=Optionally\\, the end time of the time range to filter the results by"`
296299
Limit int `json:"limit,omitempty" jsonschema:"default=100,description=Optionally\\, the maximum number of results to return"`
300+
ProjectName string `json:"projectName,omitempty" jsonschema:"description=GCP project name to query (Cloud Monitoring datasources only). Overrides or substitutes the defaultProject configured on the datasource."`
297301
}
298302

299303
func listPrometheusLabelNames(ctx context.Context, args ListPrometheusLabelNamesParams) ([]string, error) {
300-
backend, err := backendForDatasource(ctx, args.DatasourceUID)
304+
backend, err := backendForDatasource(ctx, args.DatasourceUID, args.ProjectName)
301305
if err != nil {
302306
return nil, fmt.Errorf("getting backend: %w", err)
303307
}
@@ -353,10 +357,11 @@ type ListPrometheusLabelValuesParams struct {
353357
StartRFC3339 string `json:"startRfc3339,omitempty" jsonschema:"description=Optionally\\, the start time of the query"`
354358
EndRFC3339 string `json:"endRfc3339,omitempty" jsonschema:"description=Optionally\\, the end time of the query"`
355359
Limit int `json:"limit,omitempty" jsonschema:"default=100,description=Optionally\\, the maximum number of results to return"`
360+
ProjectName string `json:"projectName,omitempty" jsonschema:"description=GCP project name to query (Cloud Monitoring datasources only). Overrides or substitutes the defaultProject configured on the datasource."`
356361
}
357362

358363
func listPrometheusLabelValues(ctx context.Context, args ListPrometheusLabelValuesParams) ([]string, error) {
359-
backend, err := backendForDatasource(ctx, args.DatasourceUID)
364+
backend, err := backendForDatasource(ctx, args.DatasourceUID, args.ProjectName)
360365
if err != nil {
361366
return nil, fmt.Errorf("getting backend: %w", err)
362367
}
@@ -422,6 +427,7 @@ type QueryPrometheusHistogramParams struct {
422427
StartTime string `json:"startTime,omitempty" jsonschema:"description=Start time (default: now-1h). Supports RFC3339\\, relative (now-1h)\\, or Unix ms."`
423428
EndTime string `json:"endTime,omitempty" jsonschema:"description=End time (default: now). Supports RFC3339\\, relative\\, or Unix ms."`
424429
StepSeconds int `json:"stepSeconds,omitempty" jsonschema:"description=Step size in seconds for range query (default: 60)"`
430+
ProjectName string `json:"projectName,omitempty" jsonschema:"description=GCP project name to query (Cloud Monitoring datasources only). Overrides or substitutes the defaultProject configured on the datasource."`
425431
}
426432

427433
// queryPrometheusHistogram generates and executes a histogram percentile query
@@ -483,6 +489,7 @@ func queryPrometheusHistogram(ctx context.Context, args QueryPrometheusHistogram
483489
EndTime: endTime,
484490
StepSeconds: stepSeconds,
485491
QueryType: "range",
492+
ProjectName: args.ProjectName,
486493
})
487494
if err != nil {
488495
return nil, err

0 commit comments

Comments
 (0)