Skip to content

Commit fbabf75

Browse files
authored
refactor: extract Grafana Client creation from context functions (#127)
* refactor: extract Grafana Client creation from context functions There was a bunch of duplicated code when creating the Grafana client from context, and I want to be able to do this in tests too, so this just refactors the code for later. * test: add test case for finding log error patterns (#128) * Add log lines when creating Sift investigation Should help debug issues when running Sift tools. * Include Grafana client in cloud test context Using the refactored NewGrafanaClient function from PR #127. * Fetch multiple investigations in tests, in case the first one has no analyses This can happen if, for example, an investigation was run with a scope that didn't find any logs, and so no analyses were created. * Add test for finding error pattern logs This relies on a new Loki datasource being added to the mcptests instance, which points to our dev logs. We probably want to get rid of this and use different logs; I'll look into how we can do that.
1 parent 37e0754 commit fbabf75

File tree

4 files changed

+69
-32
lines changed

4 files changed

+69
-32
lines changed

mcpgrafana.go

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -154,19 +154,18 @@ func makeBasePath(path string) string {
154154
return strings.Join([]string{strings.TrimRight(path, "/"), "api"}, "/")
155155
}
156156

157-
// ExtractGrafanaClientFromEnv is a StdioContextFunc that extracts Grafana configuration
158-
// from environment variables and injects a configured client into the context.
159-
var ExtractGrafanaClientFromEnv server.StdioContextFunc = func(ctx context.Context) context.Context {
157+
// NewGrafanaClient creates a Grafana client with the provided URL and API key,
158+
// configured to use the correct scheme and debug mode.
159+
func NewGrafanaClient(ctx context.Context, grafanaURL, apiKey string) *client.GrafanaHTTPAPI {
160160
cfg := client.DefaultTransportConfig()
161-
// Extract transport config from env vars, and set it on the context.
162-
var grafanaURL string
161+
163162
var parsedURL *url.URL
164-
var ok bool
165163
var err error
166-
if grafanaURL, ok = os.LookupEnv(grafanaURLEnvVar); ok {
164+
165+
if grafanaURL != "" {
167166
parsedURL, err = url.Parse(grafanaURL)
168167
if err != nil {
169-
panic(fmt.Errorf("invalid %s: %w", grafanaURLEnvVar, err))
168+
panic(fmt.Errorf("invalid Grafana URL: %w", err))
170169
}
171170
cfg.Host = parsedURL.Host
172171
cfg.BasePath = makeBasePath(parsedURL.Path)
@@ -179,22 +178,33 @@ var ExtractGrafanaClientFromEnv server.StdioContextFunc = func(ctx context.Conte
179178
parsedURL, _ = url.Parse(defaultGrafanaURL)
180179
}
181180

182-
apiKey := os.Getenv(grafanaAPIEnvVar)
183181
if apiKey != "" {
184182
cfg.APIKey = apiKey
185183
}
186184

187185
cfg.Debug = GrafanaDebugFromContext(ctx)
188186

189187
slog.Debug("Creating Grafana client", "url", parsedURL.Redacted(), "api_key_set", apiKey != "")
190-
client := client.NewHTTPClientWithConfig(strfmt.Default, cfg)
191-
return context.WithValue(ctx, grafanaClientKey{}, client)
188+
return client.NewHTTPClientWithConfig(strfmt.Default, cfg)
189+
}
190+
191+
// ExtractGrafanaClientFromEnv is a StdioContextFunc that extracts Grafana configuration
192+
// from environment variables and injects a configured client into the context.
193+
var ExtractGrafanaClientFromEnv server.StdioContextFunc = func(ctx context.Context) context.Context {
194+
// Extract transport config from env vars
195+
grafanaURL, ok := os.LookupEnv(grafanaURLEnvVar)
196+
if !ok {
197+
grafanaURL = defaultGrafanaURL
198+
}
199+
apiKey := os.Getenv(grafanaAPIEnvVar)
200+
201+
grafanaClient := NewGrafanaClient(ctx, grafanaURL, apiKey)
202+
return context.WithValue(ctx, grafanaClientKey{}, grafanaClient)
192203
}
193204

194205
// ExtractGrafanaClientFromHeaders is a HTTPContextFunc that extracts Grafana configuration
195206
// from request headers and injects a configured client into the context.
196207
var ExtractGrafanaClientFromHeaders server.HTTPContextFunc = func(ctx context.Context, req *http.Request) context.Context {
197-
cfg := client.DefaultTransportConfig()
198208
// Extract transport config from request headers, and set it on the context.
199209
u, apiKey := urlAndAPIKeyFromHeaders(req)
200210
uEnv, apiKeyEnv := urlAndAPIKeyFromEnv()
@@ -204,23 +214,9 @@ var ExtractGrafanaClientFromHeaders server.HTTPContextFunc = func(ctx context.Co
204214
if apiKey == "" {
205215
apiKey = apiKeyEnv
206216
}
207-
if u != "" {
208-
if url, err := url.Parse(u); err == nil {
209-
cfg.Host = url.Host
210-
cfg.BasePath = makeBasePath(url.Path)
211-
if url.Scheme == "http" {
212-
cfg.Schemes = []string{"http"}
213-
}
214-
}
215-
}
216-
if apiKey != "" {
217-
cfg.APIKey = apiKey
218-
}
219-
220-
cfg.Debug = GrafanaDebugFromContext(ctx)
221217

222-
client := client.NewHTTPClientWithConfig(strfmt.Default, cfg)
223-
return WithGrafanaClient(ctx, client)
218+
grafanaClient := NewGrafanaClient(ctx, u, apiKey)
219+
return WithGrafanaClient(ctx, grafanaClient)
224220
}
225221

226222
// WithGrafanaClient sets the Grafana client in the context.

tools/cloud_testing_utils.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ import (
1111
mcpgrafana "github.com/grafana/mcp-grafana"
1212
)
1313

14-
// createCloudTestContext creates a context with Grafana URL and API key for cloud integration tests.
14+
// createCloudTestContext creates a context with a Grafana URL, Grafana API key and
15+
// Grafana client for cloud integration tests.
1516
// The test will be skipped if required environment variables are not set.
1617
// testName is used to customize the skip message (e.g. "OnCall", "Sift", "Incident")
1718
// urlEnv and apiKeyEnv specify the environment variable names for the Grafana URL and API key.
1819
func createCloudTestContext(t *testing.T, testName, urlEnv, apiKeyEnv string) context.Context {
20+
ctx := context.Background()
21+
1922
grafanaURL := os.Getenv(urlEnv)
2023
if grafanaURL == "" {
2124
t.Skipf("%s environment variable not set, skipping cloud %s integration tests", urlEnv, testName)
@@ -26,9 +29,11 @@ func createCloudTestContext(t *testing.T, testName, urlEnv, apiKeyEnv string) co
2629
t.Skipf("%s environment variable not set, skipping cloud %s integration tests", apiKeyEnv, testName)
2730
}
2831

29-
ctx := context.Background()
32+
client := mcpgrafana.NewGrafanaClient(ctx, grafanaURL, grafanaApiKey)
33+
3034
ctx = mcpgrafana.WithGrafanaURL(ctx, grafanaURL)
3135
ctx = mcpgrafana.WithGrafanaAPIKey(ctx, grafanaApiKey)
36+
ctx = mcpgrafana.WithGrafanaClient(ctx, client)
3237

3338
return ctx
3439
}

tools/sift.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"fmt"
88
"io"
9+
"log/slog"
910
"net/http"
1011
"time"
1112

@@ -288,6 +289,7 @@ func findErrorPatternLogs(ctx context.Context, args FindErrorPatternLogsParams)
288289
}
289290

290291
// Get all analyses from the completed investigation
292+
slog.Debug("Getting analyses", "investigation_id", completedInvestigation.ID)
291293
analyses, err := client.getSiftAnalyses(ctx, completedInvestigation.ID)
292294
if err != nil {
293295
return nil, fmt.Errorf("getting analyses: %w", err)
@@ -305,6 +307,7 @@ func findErrorPatternLogs(ctx context.Context, args FindErrorPatternLogsParams)
305307
if errorPatternLogsAnalysis == nil {
306308
return nil, fmt.Errorf("ErrorPatternLogs analysis not found in investigation %s", completedInvestigation.ID)
307309
}
310+
slog.Debug("Found ErrorPatternLogs analysis", "analysis_id", errorPatternLogsAnalysis.ID)
308311

309312
datasourceUID := completedInvestigation.Datasources.LokiDatasource.UID
310313

@@ -485,10 +488,12 @@ func (c *siftClient) createSiftInvestigation(ctx context.Context, investigation
485488
return nil, fmt.Errorf("marshaling investigation: %w", err)
486489
}
487490

491+
slog.Debug("Creating investigation", "payload", string(jsonData))
488492
buf, err := c.makeRequest(ctx, "POST", "/api/plugins/grafana-ml-app/resources/sift/api/v1/investigations", jsonData)
489493
if err != nil {
490494
return nil, err
491495
}
496+
slog.Debug("Investigation created", "response", string(buf))
492497

493498
investigationResponse := struct {
494499
Status string `json:"status"`
@@ -512,6 +517,7 @@ func (c *siftClient) createSiftInvestigation(ctx context.Context, investigation
512517
case <-timeout:
513518
return nil, fmt.Errorf("timeout waiting for investigation completion after 5 minutes")
514519
case <-ticker.C:
520+
slog.Debug("Polling investigation status", "investigation_id", investigationResponse.Data.ID)
515521
investigation, err := c.getSiftInvestigation(ctx, investigationResponse.Data.ID)
516522
if err != nil {
517523
return nil, err

tools/sift_cloud_test.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ package tools
1111

1212
import (
1313
"testing"
14+
"time"
1415

1516
"github.com/stretchr/testify/assert"
1617
"github.com/stretchr/testify/require"
@@ -49,11 +50,19 @@ func TestCloudSiftInvestigations(t *testing.T) {
4950
})
5051

5152
// Get an investigation ID from the list to test getting a specific investigation
52-
investigations, err := listSiftInvestigations(ctx, ListSiftInvestigationsParams{Limit: 1})
53+
investigations, err := listSiftInvestigations(ctx, ListSiftInvestigationsParams{Limit: 10})
5354
require.NoError(t, err, "Should not error when listing investigations")
5455
require.NotEmpty(t, investigations, "Should have at least one investigation to test with")
5556

56-
investigationID := investigations[0].ID.String()
57+
// Find an investigation with at least one analysis.
58+
var investigationID string
59+
for _, investigation := range investigations {
60+
if len(investigation.Analyses.Items) > 0 {
61+
investigationID = investigation.ID.String()
62+
break
63+
}
64+
}
65+
require.NotEmpty(t, investigationID, "Should have at least one investigation with at least one analysis")
5766

5867
// Test getting a specific investigation
5968
t.Run("get specific investigation", func(t *testing.T) {
@@ -105,4 +114,25 @@ func TestCloudSiftInvestigations(t *testing.T) {
105114
assert.NotEmpty(t, analysis.InvestigationID, "Analysis should have an investigation ID")
106115
assert.NotNil(t, analysis.Result, "Analysis should have a result")
107116
})
117+
118+
t.Run("find error patterns", func(t *testing.T) {
119+
// Find error patterns
120+
analysis, err := findErrorPatternLogs(ctx, FindErrorPatternLogsParams{
121+
Name: "Test Sift",
122+
Labels: map[string]string{
123+
"namespace": "hosted-grafana",
124+
"cluster": "dev-eu-west-2",
125+
"slug": "mcptests",
126+
},
127+
Start: time.Now().Add(-5 * time.Minute),
128+
End: time.Now(),
129+
})
130+
require.NoError(t, err, "Should not error when finding error patterns")
131+
assert.NotNil(t, analysis, "Result should not be nil")
132+
133+
// Verify all required fields are present
134+
assert.NotEmpty(t, analysis.Name, "Analysis should have a name")
135+
assert.NotEmpty(t, analysis.InvestigationID, "Analysis should have an investigation ID")
136+
assert.NotEmpty(t, analysis.Result.Message, "Analysis should have a message")
137+
})
108138
}

0 commit comments

Comments
 (0)