Skip to content

Commit 743a7f3

Browse files
sunkersarahzingerfridgepoet
authored
Filter on account id (#157)
* Filter service graph by account id * Refactor to create a separate resources route * Use a map rather than looping everytime * Fixes after CR: - refactor /accountIds to /accounts to be more future proof - make a for loop more readable - put work behind feature flag - move call to fetch account ids within QueryEditorForm - fix bug to reset selection, if account id is no longer a valid option - fix bug to handle edges with account ids with targets that do not have account ids - remove accidentally left comments. * WIP * More pr fixes and adding in autocomplete * Fix test * Fix backend linter * Fix backend test * Comment out frontend tests * Modify plugin.json id * Fix bug about group name and fix broken test * Fix go test * prepare release * Merge pull request #3 from grafana/lattice-docs Add some documentation for cross account queries * Merge main, fix conflicts * Remove duplicate test from merge conflict * revert plugin id Co-authored-by: Sarah Zinger <sarah.zinger@grafana.com> Co-authored-by: Shirley Leu <4163034+fridgepoet@users.noreply.github.com>
1 parent 2d53b57 commit 743a7f3

16 files changed

+286
-66
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
All notable changes to this project will be documented in this file.
44

5-
## 2.2.0
5+
## 2.3.0
6+
- Feature: Make it possible to filter on account id in https://github.com/grafana/x-ray-datasource/pull/157
67

8+
## 2.2.0
79
- Make properties of `SummaryStatistics` optional
810

911
## 2.1.2

README.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ In Insights you can see the summary table for Insights. Clicking the InsightId w
146146

147147
Service map in Grafana enables customers to view the map of their applications built using microservices architecture. Each node on the map represents a service such as an AWS Lambda function or API running on API Gateway or a DynamoDB table. With this map, customers can easily detect performance issues, or increase in error, fault or throttle rates in any of their services and dive deep into corresponding traces and root cause.
148148

149-
![Service map](https://grafana.com/static/img/docs/node-graph/node-graph-7-4.png "Service map")
149+
![Service map](https://grafana.com/static/img/docs/node-graph/node-graph-7-4.png 'Service map')
150150

151151
Service Map query type shows the same data as a service map inside X-ray console.
152152

@@ -158,8 +158,7 @@ To display the service map:
158158

159159
You can pan and zoom the view with buttons or you mouse. For details about the visualization, refer to [Node graph panel](https://grafana.com/docs/grafana/latest/panels/visualizations/node-graph/).
160160

161-
![Service map navigation](https://storage.googleapis.com/integration-artifacts/grafana-x-ray-datasource/screenshots/x-ray-service-map-nav.gif "Service map navigation")
162-
161+
![Service map navigation](https://storage.googleapis.com/integration-artifacts/grafana-x-ray-datasource/screenshots/x-ray-service-map-nav.gif 'Service map navigation')
163162

164163
Similar to X-ray root nodes, nodes in the service map representing the client application are on the left side of the map.
165164

@@ -221,3 +220,22 @@ datasources:
221220
accessKey: '<your access key>'
222221
secretKey: '<your secret key>'
223222
```
223+
224+
## Cross-Account Observability
225+
226+
The X-Ray plugin allows you to monitor traces across multiple AWS accounts within a region with a feature called Cross-Account Observability. Using cross-account observability, you can seamlessly search, visualize and analyze AWS traces without worrying about account boundaries.
227+
228+
### Getting started
229+
230+
To enable cross-account observability, first enable the feature in AWS using the official [CloudWatch docs](http://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Unified-Cross-Account.html), then add [two new API actions]({{< relref "../Metrics_and_Logs" >}}) to the IAM policy attached to the role/user running the plugin.
231+
232+
This feature is currently behind the `cloudWatchCrossAccountQuerying` feature toggle.
233+
234+
> You can enable feature toggles through configuration file or environment variables. See configuration [docs]({{< relref "../setup-grafana/configure-grafana/#feature_toggles" >}}) for details.
235+
> Grafana Cloud users can access this feature by [opening a support ticket in the Cloud Portal](https://grafana.com/profile/org#support).
236+
237+
## Filtering Traces by Account Id
238+
239+
Once the feature is enabled, you will be able to both display traces across multiple accounts and filter those traces by account ID. When you select the Service Map query type in Grafana, an account dropdown displays and populates with the account IDs shown in the traces returned in the selected time frame.
240+
241+
You can also add account ID as part of a query filter expression in the Trace List query type.

pkg/datasource/datasource.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ func NewDatasource(xrayClientFactory XrayClientFactory) *Datasource {
103103
resMux := http.NewServeMux()
104104
resMux.HandleFunc("/groups", ds.getGroups)
105105
resMux.HandleFunc("/regions", ds.GetRegions)
106+
resMux.HandleFunc("/accounts", ds.GetAccounts)
107+
106108
ds.ResourceMux = httpadapter.New(resMux)
107109
return ds
108110
}

pkg/datasource/datasource_test.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,14 @@ func (client *XrayClientMock) GetServiceGraphPagesWithContext(ctx aws.Context, i
2727
output := &xray.GetServiceGraphOutput{
2828
NextToken: nil,
2929
Services: []*xray.Service{
30-
{Name: aws.String(serviceName)},
30+
{
31+
Name: aws.String(serviceName),
32+
AccountId: aws.String("testAccount1"),
33+
},
34+
{
35+
Name: aws.String(serviceName + "2"),
36+
AccountId: aws.String("testAccount2"),
37+
},
3138
},
3239
}
3340
fn(output, false)
@@ -527,7 +534,7 @@ func TestDatasource(t *testing.T) {
527534

528535
// Bit simplistic test but right now we just send each service as a json to frontend and do transform there.
529536
frame := response.Responses["A"].Frames[0]
530-
require.Equal(t, 1, frame.Fields[0].Len())
537+
require.Equal(t, 2, frame.Fields[0].Len()) // 2 because of the 2 services added to the mock
531538
})
532539

533540
t.Run("getServiceMap query with region", func(t *testing.T) {
@@ -537,7 +544,7 @@ func TestDatasource(t *testing.T) {
537544

538545
// Bit simplistic test but right now we just send each service as a json to frontend and do transform there.
539546
frame := response.Responses["A"].Frames[0]
540-
require.Equal(t, 1, frame.Fields[0].Len())
547+
require.Equal(t, 2, frame.Fields[0].Len())
541548
require.True(t, strings.Contains(frame.Fields[0].At(0).(string), "mockServiceName-us-east-1"))
542549
})
543550

pkg/datasource/getAccounts.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package datasource
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"net/url"
7+
"time"
8+
9+
"github.com/aws/aws-sdk-go/service/xray"
10+
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
11+
)
12+
13+
type Account struct {
14+
Id string
15+
}
16+
17+
func (ds *Datasource) GetAccounts(rw http.ResponseWriter, req *http.Request) {
18+
if req.Method != "GET" {
19+
rw.WriteHeader(http.StatusMethodNotAllowed)
20+
return
21+
}
22+
urlQuery, err := url.ParseQuery(req.URL.RawQuery)
23+
if err != nil {
24+
sendError(rw, err)
25+
return
26+
}
27+
region := urlQuery.Get("region")
28+
29+
pluginConfig := httpadapter.PluginConfigFromContext(req.Context())
30+
xrayClient, err := ds.xrayClientFactory(&pluginConfig, RequestSettings{Region: region})
31+
32+
if err != nil {
33+
sendError(rw, err)
34+
return
35+
}
36+
37+
group := urlQuery.Get("group")
38+
39+
layout := "2006-01-02T15:04:05.000Z"
40+
startTime, err := time.Parse(layout, urlQuery.Get("startTime"))
41+
if err != nil {
42+
sendError(rw, err)
43+
return
44+
}
45+
46+
endTime, err := time.Parse(layout, urlQuery.Get("endTime"))
47+
if err != nil {
48+
sendError(rw, err)
49+
return
50+
}
51+
52+
input := &xray.GetServiceGraphInput{
53+
StartTime: &startTime,
54+
EndTime: &endTime,
55+
GroupName: &group,
56+
}
57+
58+
accounts := []Account{}
59+
60+
err = xrayClient.GetServiceGraphPagesWithContext(req.Context(), input, func(page *xray.GetServiceGraphOutput, lastPage bool) bool {
61+
for _, service := range page.Services {
62+
if service.AccountId != nil {
63+
account := Account{
64+
Id: *service.AccountId,
65+
}
66+
accounts = append(accounts, account)
67+
}
68+
}
69+
70+
// Not sure how many pages there can possibly be but for now try to iterate over all the pages.
71+
return true
72+
})
73+
if err != nil {
74+
sendError(rw, err)
75+
return
76+
}
77+
78+
body, err := json.Marshal(accounts)
79+
if err != nil {
80+
sendError(rw, err)
81+
return
82+
}
83+
84+
rw.Header().Set("content-type", "application/json")
85+
_, err = rw.Write(body)
86+
if err != nil {
87+
sendError(rw, err)
88+
return
89+
}
90+
}

pkg/datasource/getAccounts_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package datasource_test
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/grafana/x-ray-datasource/pkg/datasource"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
type Account struct {
14+
Id string
15+
}
16+
17+
func TestAccounts(t *testing.T) {
18+
t.Run("when passed a get request it returns a list of all accountIds in the traces in the selected time frame", func(t *testing.T) {
19+
ds := datasource.NewDatasource(xrayClientFactory)
20+
req := httptest.NewRequest("GET", "http://example.com/accounts?startTime=2022-09-23T00:15:14.365Z&endTime=2022-09-23T01:15:14.365Z&group=somegroup", nil)
21+
w := httptest.NewRecorder()
22+
ds.GetAccounts(w, req)
23+
resp := w.Result()
24+
body, _ := io.ReadAll(resp.Body)
25+
accounts := []Account{}
26+
require.NoError(t, json.Unmarshal(body, &accounts))
27+
require.Contains(t, accounts[0].Id, "testAccount1")
28+
require.Contains(t, accounts[1].Id, "testAccount2")
29+
})
30+
}

pkg/datasource/getServiceMap.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import (
1111
)
1212

1313
type GetServiceMapQueryData struct {
14-
Region string `json:"region"`
15-
Group *xray.Group `json:"group"`
14+
Region string `json:"region"`
15+
Group *xray.Group `json:"group"`
16+
AccountIds []string `json:"accountIds,omitempty"`
1617
}
1718

1819
func (ds *Datasource) getServiceMap(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
@@ -57,8 +58,25 @@ func (ds *Datasource) getSingleServiceMap(ctx context.Context, query backend.Dat
5758
EndTime: &query.TimeRange.To,
5859
GroupName: queryData.Group.GroupName,
5960
}
61+
62+
accountIdsToFilterBy := make(map[string]bool)
63+
for _, value := range queryData.AccountIds {
64+
accountIdsToFilterBy[value] = true
65+
}
66+
6067
err = xrayClient.GetServiceGraphPagesWithContext(ctx, input, func(page *xray.GetServiceGraphOutput, lastPage bool) bool {
6168
for _, service := range page.Services {
69+
// filter out non matching account ids, if user has selected them
70+
if len(queryData.AccountIds) > 0 {
71+
// sometimes traces don't have accountId data, without knowing where it came from we have to filter it out
72+
if service.AccountId == nil {
73+
continue
74+
}
75+
76+
if !accountIdsToFilterBy[*service.AccountId] {
77+
continue
78+
}
79+
}
6280
bytes, err := json.Marshal(service)
6381
if err != nil {
6482
// TODO: probably does not make sense to fail just because of one service but I assume the layout will fail

src/DataSource.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
TimeRange,
1111
toDuration,
1212
} from '@grafana/data';
13-
import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
13+
import { DataSourceWithBackend, getTemplateSrv, TemplateSrv, config } from '@grafana/runtime';
1414
import { Observable } from 'rxjs';
1515
import { map } from 'rxjs/operators';
1616

@@ -61,11 +61,11 @@ export class XrayDataSource extends DataSourceWithBackend<XrayQuery, XrayJsonDat
6161
const params = new URLSearchParams({ region });
6262
searchString = '?' + params.toString();
6363
}
64-
return this.getResource(`/groups${searchString}`);
64+
return this.getResource(`groups${searchString}`);
6565
}
6666

6767
async getRegions(): Promise<Region[]> {
68-
const response = await this.getResource('/regions');
68+
const response = await this.getResource('regions');
6969
return [
7070
...sortBy(
7171
response.map((name: string) => ({
@@ -78,6 +78,22 @@ export class XrayDataSource extends DataSourceWithBackend<XrayQuery, XrayJsonDat
7878
];
7979
}
8080

81+
async getAccountIdsForServiceMap(range?: TimeRange, group?: Group): Promise<string[]> {
82+
if (!config.featureToggles.cloudWatchCrossAccountQuerying) {
83+
return [];
84+
}
85+
const params = new URLSearchParams({
86+
startTime: range ? range.from.toISOString() : '',
87+
endTime: range ? range.to.toISOString() : '',
88+
group: group?.GroupName || 'Default',
89+
});
90+
91+
const searchString = '?' + params.toString();
92+
93+
const response = await this.getResource(`accounts${searchString}`);
94+
return response.map((account: { Id: string }) => account.Id);
95+
}
96+
8197
getServiceMapUrl(region?: string): string {
8298
return `${this.getXrayUrl(region)}#/service-map/`;
8399
}

src/components/QueryEditor/QueryEditor.test.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ const defaultProps = {
3939
async getRegions(): Promise<Region[]> {
4040
return [{ label: 'region1', text: 'region1', value: 'region1' }];
4141
},
42+
async getAccountIdsForServiceMap(): Promise<string[]> {
43+
return ['account1', 'account2'];
44+
},
4245
getServiceMapUrl() {
4346
return 'service-map';
4447
},
@@ -153,9 +156,7 @@ describe('QueryEditor', () => {
153156

154157
fireEvent.change(field, { target: { value: '1-5f160a8b-83190adad07f429219c0e259' } });
155158

156-
// First call would be the query init call. We do not update the query based on that so when doing this second one
157-
// it's done without default region or group.
158-
expect(onChange.mock.calls[1][0]).toEqual({
159+
expect(onChange.mock.calls[3][0]).toEqual({
159160
refId: 'A',
160161
query: '1-5f160a8b-83190adad07f429219c0e259',
161162
queryType: XrayQueryType.getTrace,
@@ -216,6 +217,26 @@ describe('QueryEditor', () => {
216217
serviceMap: 'https://region2.console.aws.amazon.com/xray/home?region=region2#/service-map/',
217218
});
218219
});
220+
221+
it('shows the accountIds in a dropdown on service map selection', async () => {
222+
await act(async () => {
223+
render(
224+
<QueryEditor
225+
{...{
226+
...defaultProps,
227+
query: {
228+
refId: 'A',
229+
queryType: 'getServiceMap',
230+
accountIds: ['account1'],
231+
} as any,
232+
}}
233+
onChange={() => {}}
234+
/>
235+
);
236+
expect(screen.getByText('', { selector: '.fa-spinner' })).toBeDefined();
237+
await waitFor(() => expect(screen.getByText('account1')).toBeDefined());
238+
});
239+
});
219240
});
220241

221242
function makeDataSource(settings: DataSourceInstanceSettings<XrayJsonData>) {

src/components/QueryEditor/QueryEditor.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { Spinner } from '@grafana/ui';
33
import { useGroups } from './useGroups';
44
import { QueryEditorForm, XrayQueryEditorFormProps } from './QueryEditorForm';
55
import { useRegions } from './useRegions';
6-
76
/**
87
* Simple wrapper that is only responsible to load groups and delay actual render of the QueryEditorForm. Main reason
98
* for that is that there is queryInit code that requires groups to be already loaded and is separate hook and it

0 commit comments

Comments
 (0)