-
Notifications
You must be signed in to change notification settings - Fork 101
First Asserts tool #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
First Asserts tool #105
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
73dda54
First Asserts tool
xujiaxj 2418340
Try to fix the test
xujiaxj 771591e
Only cloud test Asserts
xujiaxj 6b177bc
Only unit test
xujiaxj 4126c01
Fix assertion list
xujiaxj 7a63d6b
fix unit test
xujiaxj 7b23410
Resolve merge conflicts in main.go: combine enabledTools handling wit…
xujiaxj 1062420
revert back to use RFC3339 time as input
xujiaxj 7faa70c
Resolve merge conflicts in main.go: include both asserts and sift tools
xujiaxj 7274e52
Address review comments
xujiaxj 3d555ba
Fix lint errors
xujiaxj 96d3e84
Use time.Time in params struct
sd2k 0466e4f
Make env/site/namespace optional
xujiaxj 12e5eb8
Add asserts cloud test
xujiaxj 97e3a3f
Refactor cloud tests
xujiaxj File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
sd2k marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
package tools | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"strings" | ||
"time" | ||
|
||
mcpgrafana "github.com/grafana/mcp-grafana" | ||
"github.com/mark3labs/mcp-go/server" | ||
) | ||
|
||
func newAssertsClient(ctx context.Context) (*Client, error) { | ||
grafanaURL, apiKey := mcpgrafana.GrafanaURLFromContext(ctx), mcpgrafana.GrafanaAPIKeyFromContext(ctx) | ||
url := fmt.Sprintf("%s/api/plugins/grafana-asserts-app/resources/asserts/api-server", strings.TrimRight(grafanaURL, "/")) | ||
|
||
client := &http.Client{ | ||
Transport: &authRoundTripper{ | ||
apiKey: apiKey, | ||
underlying: http.DefaultTransport, | ||
}, | ||
} | ||
|
||
return &Client{ | ||
httpClient: client, | ||
baseURL: url, | ||
}, nil | ||
} | ||
|
||
type GetAssertionsParams struct { | ||
StartTime time.Time `json:"startTime" jsonschema:"required,description=The start time in RFC3339 format"` | ||
EndTime time.Time `json:"endTime" jsonschema:"required,description=The end time in RFC3339 format"` | ||
EntityType string `json:"entityType" jsonschema:"description=The type of the entity to list (e.g. Service\\, Node\\, Pod\\, etc.)"` | ||
EntityName string `json:"entityName" jsonschema:"description=The name of the entity to list"` | ||
Env string `json:"env,omitempty" jsonschema:"description=The env of the entity to list"` | ||
Site string `json:"site,omitempty" jsonschema:"description=The site of the entity to list"` | ||
Namespace string `json:"namespace,omitempty" jsonschema:"description=The namespace of the entity to list"` | ||
} | ||
|
||
type scope struct { | ||
Env string `json:"env,omitempty"` | ||
Site string `json:"site,omitempty"` | ||
Namespace string `json:"namespace,omitempty"` | ||
} | ||
|
||
type entity struct { | ||
Name string `json:"name"` | ||
Type string `json:"type"` | ||
Scope scope `json:"scope"` | ||
} | ||
|
||
type requestBody struct { | ||
StartTime int64 `json:"startTime"` | ||
EndTime int64 `json:"endTime"` | ||
EntityKeys []entity `json:"entityKeys"` | ||
SuggestionSrcEntities []entity `json:"suggestionSrcEntities"` | ||
AlertCategories []string `json:"alertCategories"` | ||
} | ||
|
||
func (c *Client) fetchAssertsData(ctx context.Context, urlPath string, method string, reqBody any) (string, error) { | ||
jsonData, err := json.Marshal(reqBody) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to marshal request body: %w", err) | ||
} | ||
|
||
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+urlPath, bytes.NewBuffer(jsonData)) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to create request: %w", err) | ||
} | ||
req.Header.Set("Content-Type", "application/json") | ||
|
||
resp, err := c.httpClient.Do(req) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to execute request: %w", err) | ||
} | ||
defer resp.Body.Close() | ||
|
||
body, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to read response body: %w", err) | ||
} | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return "", fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(body)) | ||
} | ||
|
||
return string(body), nil | ||
} | ||
|
||
func getAssertions(ctx context.Context, args GetAssertionsParams) (string, error) { | ||
client, err := newAssertsClient(ctx) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to create Asserts client: %w", err) | ||
} | ||
|
||
// Create request body | ||
reqBody := requestBody{ | ||
StartTime: args.StartTime.UnixMilli(), | ||
EndTime: args.EndTime.UnixMilli(), | ||
EntityKeys: []entity{ | ||
{ | ||
Name: args.EntityName, | ||
Type: args.EntityType, | ||
Scope: scope{}, | ||
}, | ||
}, | ||
SuggestionSrcEntities: []entity{}, | ||
AlertCategories: []string{"saturation", "amend", "anomaly", "failure", "error"}, | ||
} | ||
|
||
if args.Env != "" { | ||
reqBody.EntityKeys[0].Scope.Env = args.Env | ||
} | ||
if args.Site != "" { | ||
reqBody.EntityKeys[0].Scope.Site = args.Site | ||
} | ||
if args.Namespace != "" { | ||
reqBody.EntityKeys[0].Scope.Namespace = args.Namespace | ||
} | ||
|
||
data, err := client.fetchAssertsData(ctx, "/v1/assertions/llm-summary", "POST", reqBody) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to fetch data: %w", err) | ||
} | ||
|
||
return data, nil | ||
} | ||
|
||
var GetAssertions = mcpgrafana.MustTool( | ||
"get_assertions", | ||
"Get assertion summary for a given entity with its type, name, env, site, namespace, and a time range", | ||
getAssertions, | ||
) | ||
|
||
func AddAssertsTools(mcp *server.MCPServer) { | ||
GetAssertions.Register(mcp) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
//go:build cloud | ||
// +build cloud | ||
|
||
// This file contains cloud integration tests that run against a dedicated test instance | ||
// connected to a Grafana instance at (ASSERTS_GRAFANA_URL, ASSERTS_GRAFANA_API_KEY). | ||
// These tests expect this configuration to exist and will skip if the required | ||
// environment variables are not set. | ||
|
||
package tools | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestAssertsCloudIntegration(t *testing.T) { | ||
ctx := createCloudTestContext(t, "Asserts", "ASSERTS_GRAFANA_URL", "ASSERTS_GRAFANA_API_KEY") | ||
|
||
t.Run("get assertions", func(t *testing.T) { | ||
// Set up time range for the last hour | ||
endTime := time.Now() | ||
startTime := endTime.Add(-24 * time.Hour) | ||
|
||
// Test parameters for a known service in the environment | ||
params := GetAssertionsParams{ | ||
StartTime: startTime, | ||
EndTime: endTime, | ||
EntityType: "Service", // Adjust these values based on your actual environment | ||
EntityName: "model-builder", | ||
Env: "dev-us-central-0", | ||
Namespace: "asserts", | ||
} | ||
|
||
// Get assertions from the real Grafana instance | ||
result, err := getAssertions(ctx, params) | ||
require.NoError(t, err, "Failed to get assertions from Grafana") | ||
assert.NotEmpty(t, result, "Expected non-empty assertions result") | ||
|
||
// Basic validation of the response structure | ||
assert.Contains(t, result, "summaries", "Response should contain a summaries field") | ||
}) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
//go:build unit | ||
// +build unit | ||
|
||
package tools | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
"time" | ||
|
||
mcpgrafana "github.com/grafana/mcp-grafana" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func setupMockAssertsServer(handler http.HandlerFunc) (*httptest.Server, context.Context) { | ||
server := httptest.NewServer(handler) | ||
ctx := context.Background() | ||
ctx = mcpgrafana.WithGrafanaURL(ctx, server.URL) | ||
ctx = mcpgrafana.WithGrafanaAPIKey(ctx, "test-api-key") | ||
return server, ctx | ||
} | ||
|
||
func TestAssertTools(t *testing.T) { | ||
t.Run("get assertions", func(t *testing.T) { | ||
startTime := time.Date(2025, 4, 23, 10, 0, 0, 0, time.UTC) | ||
endTime := time.Date(2025, 4, 23, 11, 0, 0, 0, time.UTC) | ||
server, ctx := setupMockAssertsServer(func(w http.ResponseWriter, r *http.Request) { | ||
require.Equal(t, "/api/plugins/grafana-asserts-app/resources/asserts/api-server/v1/assertions/llm-summary", r.URL.Path) | ||
require.Equal(t, "Bearer test-api-key", r.Header.Get("Authorization")) | ||
|
||
var requestBody map[string]interface{} | ||
err := json.NewDecoder(r.Body).Decode(&requestBody) | ||
require.NoError(t, err) | ||
|
||
expectedBody := map[string]interface{}{ | ||
"startTime": float64(startTime.UnixMilli()), | ||
"endTime": float64(endTime.UnixMilli()), | ||
"entityKeys": []interface{}{ | ||
map[string]interface{}{ | ||
"type": "Service", | ||
"name": "mongodb", | ||
"scope": map[string]interface{}{ | ||
"env": "asserts-demo", | ||
"site": "app", | ||
"namespace": "robot-shop", | ||
}, | ||
}, | ||
}, | ||
"suggestionSrcEntities": []interface{}{}, | ||
"alertCategories": []interface{}{"saturation", "amend", "anomaly", "failure", "error"}, | ||
} | ||
require.Equal(t, expectedBody, requestBody) | ||
|
||
w.Header().Set("Content-Type", "application/json") | ||
w.WriteHeader(http.StatusOK) | ||
_, err = w.Write([]byte(`{"summary": "test summary"}`)) | ||
require.NoError(t, err) | ||
}) | ||
defer server.Close() | ||
|
||
result, err := getAssertions(ctx, GetAssertionsParams{ | ||
StartTime: startTime, | ||
EndTime: endTime, | ||
EntityType: "Service", | ||
EntityName: "mongodb", | ||
Env: "asserts-demo", | ||
Site: "app", | ||
Namespace: "robot-shop", | ||
}) | ||
require.NoError(t, err) | ||
assert.NotNil(t, result) | ||
assert.Equal(t, `{"summary": "test summary"}`, result) | ||
}) | ||
|
||
t.Run("get assertions with no site and namespace", func(t *testing.T) { | ||
startTime := time.Date(2025, 4, 23, 10, 0, 0, 0, time.UTC) | ||
endTime := time.Date(2025, 4, 23, 11, 0, 0, 0, time.UTC) | ||
server, ctx := setupMockAssertsServer(func(w http.ResponseWriter, r *http.Request) { | ||
require.Equal(t, "/api/plugins/grafana-asserts-app/resources/asserts/api-server/v1/assertions/llm-summary", r.URL.Path) | ||
require.Equal(t, "Bearer test-api-key", r.Header.Get("Authorization")) | ||
|
||
var requestBody map[string]interface{} | ||
err := json.NewDecoder(r.Body).Decode(&requestBody) | ||
require.NoError(t, err) | ||
|
||
expectedBody := map[string]interface{}{ | ||
"startTime": float64(startTime.UnixMilli()), | ||
"endTime": float64(endTime.UnixMilli()), | ||
"entityKeys": []interface{}{ | ||
map[string]interface{}{ | ||
"type": "Service", | ||
"name": "mongodb", | ||
"scope": map[string]interface{}{ | ||
"env": "asserts-demo", | ||
}, | ||
}, | ||
}, | ||
"suggestionSrcEntities": []interface{}{}, | ||
"alertCategories": []interface{}{"saturation", "amend", "anomaly", "failure", "error"}, | ||
} | ||
require.Equal(t, expectedBody, requestBody) | ||
|
||
w.Header().Set("Content-Type", "application/json") | ||
w.WriteHeader(http.StatusOK) | ||
_, err = w.Write([]byte(`{"summary": "test summary"}`)) | ||
require.NoError(t, err) | ||
}) | ||
defer server.Close() | ||
|
||
result, err := getAssertions(ctx, GetAssertionsParams{ | ||
StartTime: startTime, | ||
EndTime: endTime, | ||
EntityType: "Service", | ||
EntityName: "mongodb", | ||
Env: "asserts-demo", | ||
}) | ||
require.NoError(t, err) | ||
assert.NotNil(t, result) | ||
assert.Equal(t, `{"summary": "test summary"}`, result) | ||
}) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.