-
Notifications
You must be signed in to change notification settings - Fork 1
test: grafana SLO #185
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
Open
bornast
wants to merge
52
commits into
master
Choose a base branch
from
test/grafana
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
test: grafana SLO #185
Changes from all commits
Commits
Show all changes
52 commits
Select commit
Hold shift + click to select a range
c9e83f6
feat: create grafana component
bornast 1df2135
docs: add grafana aws account hardcoded value comment
bornast ff2d228
refactor: provider config value extraction
bornast 02cb734
feat: remove tags prop from grafana builder and component
bornast 452c85c
refactor: rename grafana resources
bornast fd465d2
feat: grafana addDashboard builder method
bornast 37e5dde
refactor: config naming
bornast 60d26c2
Merge branch 'feat/grafana-comp' into feat/grafana-dashboards
bornast 509db81
feat: add name public prop to grafana component
bornast fd8dd9d
Merge branch 'feat/grafana-comp' into feat/grafana-dashboards
bornast 4d0839d
feat: grafana tests
bornast f8918f3
test: add custom panel test
bornast 29e7167
feat: introduce grafana connections
bornast 9aced94
refactor: remove unnecessary lines
bornast b766efe
refactor: method signatures
bornast 99376d5
feat: make grafana props readonly
bornast 50398f2
Merge branch 'feat/grafana-comp' into feat/grafana-dashboards
bornast 631ded8
feat: add name prop to grafana component
bornast 8cec815
refactor: method signatures
bornast 1626551
refactor: method signatures
bornast 075cad6
Merge branch 'feat/grafana-comp' into feat/grafana-dashboards
bornast fa0dd95
feat: generic dashboard builder
bornast d07403c
Merge branch 'master' into feat/grafana-dashboards
bornast 6a58b75
Merge branch 'master' into feat/grafana-dashboards
bornast fde5f48
refactor: dashboard build configuration
bornast 21a9b46
refactor: dashboard builder
bornast 4d46c9b
feat: add dashboard builder default configuration
bornast 813775e
refactor: panel export type
bornast a389874
refactor: panel types
bornast 0c6d123
refactor: rename prometheusNamespace to ampNamespace
bornast 410920d
Merge branch 'feat/grafana-dashboards' into test/grafana
bornast 6414b67
fix: test configuration
bornast e522d6f
refactor: panel types
bornast 36dac57
Merge branch 'master' into feat/grafana-dashboards
bornast 3049a67
refactor: error msg
bornast 518e77c
refactor: rename connection and dashboard creation methods
bornast 7f56ddf
Merge branch 'master' into feat/grafana-dashboards
bornast 5598b23
feat: add grafana folderName parameter
bornast 75a00ff
feat: make amp plugin installation optional
bornast ec0eddf
feat: make xray connection plugin installation optional
bornast 5b5c231
feat: add dataSourceName prop
bornast 46e116c
feat: add slo dashboard builder method
bornast aec0251
Merge branch 'feat/grafana-dashboards' into test/grafana
bornast a1505bb
fix: grafana test infrastructure
bornast f899b71
refactor: data source prop type
bornast a94815a
refactor: grafana dashboard name
bornast 3a98fe0
refactor: rename slo dashboard
bornast 9bf81b4
refactor: grafana builder error messages
bornast fe6f7b2
Merge branch 'feat/grafana-dashboards' into test/grafana
bornast a3e62c0
feat: introduce withTitle method inside grafana dashboard builder
bornast 28c3140
Merge branch 'master' into test/grafana
bornast a5b62f5
test: amp grafana and configurable grafana
bornast 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| import { it } from 'node:test'; | ||
| import * as assert from 'node:assert'; | ||
| import * as studion from '@studion/infra-code-blocks'; | ||
| import { | ||
| GetRoleCommand, | ||
| GetRolePolicyCommand, | ||
| ListRolePoliciesCommand, | ||
| } from '@aws-sdk/client-iam'; | ||
| import { Unwrap } from '@pulumi/pulumi'; | ||
| import { backOff } from '../util'; | ||
| import { GrafanaTestContext } from './test-context'; | ||
| import { grafanaRequest, requestEndpointWithExpectedStatus } from './util'; | ||
|
|
||
| const backOffConfig = { numOfAttempts: 15 }; | ||
|
|
||
| export function testAmpGrafana(ctx: GrafanaTestContext) { | ||
| it('should have created the AMP data source', async () => { | ||
| const grafana = ctx.outputs!.ampGrafana; | ||
| const ampDataSource = ( | ||
| grafana.connections[0] as studion.grafana.AMPConnection | ||
| ).dataSource; | ||
| const ampDataSourceName = ampDataSource.name as unknown as Unwrap< | ||
| typeof ampDataSource.name | ||
| >; | ||
|
|
||
| await backOff(async () => { | ||
| const { body, statusCode } = await grafanaRequest( | ||
| ctx, | ||
| 'GET', | ||
| `/api/datasources/name/${encodeURIComponent(ampDataSourceName)}`, | ||
| ); | ||
| assert.strictEqual(statusCode, 200, 'Expected data source to exist'); | ||
|
|
||
| const data = (await body.json()) as Record<string, unknown>; | ||
| assert.strictEqual( | ||
| data.type, | ||
| 'grafana-amazonprometheus-datasource', | ||
| 'Expected Amazon Prometheus data source type', | ||
| ); | ||
|
|
||
| const workspace = ctx.outputs!.ampWorkspace; | ||
| const ampEndpoint = workspace.prometheusEndpoint as unknown as Unwrap< | ||
| typeof workspace.prometheusEndpoint | ||
| >; | ||
| assert.ok( | ||
| (data.url as string).includes(ampEndpoint.replace(/\/$/, '')), | ||
| 'Expected data source URL to contain the AMP workspace endpoint', | ||
| ); | ||
| }, backOffConfig); | ||
| }); | ||
|
|
||
| it('should have created the dashboard with expected panels', async () => { | ||
| const dashboard = ctx.outputs!.ampGrafana.dashboards[0]; | ||
| const dashboardUid = dashboard.uid as unknown as Unwrap< | ||
| typeof dashboard.uid | ||
| >; | ||
|
|
||
| await backOff(async () => { | ||
| const { body, statusCode } = await grafanaRequest( | ||
| ctx, | ||
| 'GET', | ||
| `/api/dashboards/uid/${dashboardUid}`, | ||
| ); | ||
| assert.strictEqual(statusCode, 200, 'Expected dashboard to exist'); | ||
|
|
||
| const data = (await body.json()) as { | ||
| dashboard: { title: string; panels: Array<{ title: string }> }; | ||
| }; | ||
| assert.strictEqual( | ||
| data.dashboard.title, | ||
| 'ICB Grafana Test SLO', | ||
| 'Expected dashboard title to match', | ||
| ); | ||
|
|
||
| const panelTitles = data.dashboard.panels.map(p => p.title).sort(); | ||
| const expectedPanels = [ | ||
| 'Availability', | ||
| 'Availability Burn Rate', | ||
| 'Success Rate', | ||
| 'Success Rate Burn Rate', | ||
| 'HTTP Request Success Rate', | ||
| 'Request % below 250ms', | ||
| 'Latency Burn Rate', | ||
| '99th Percentile Latency', | ||
| 'Request percentage below 250ms', | ||
| ]; | ||
| assert.deepStrictEqual( | ||
| panelTitles, | ||
| expectedPanels.sort(), | ||
| 'Dashboard panels do not match expected panels', | ||
| ); | ||
| }, backOffConfig); | ||
| }); | ||
|
|
||
| it('should display metrics data in the dashboard', async () => { | ||
| await requestEndpointWithExpectedStatus(ctx, ctx.config.usersPath, 200); | ||
|
|
||
| const ampDataSource = ( | ||
| ctx.outputs!.ampGrafana.connections[0] as studion.grafana.AMPConnection | ||
| ).dataSource; | ||
| const ampDataSourceName = ampDataSource.name as unknown as Unwrap< | ||
| typeof ampDataSource.name | ||
| >; | ||
| const { body: dsBody } = await grafanaRequest( | ||
| ctx, | ||
| 'GET', | ||
| `/api/datasources/name/${encodeURIComponent(ampDataSourceName)}`, | ||
| ); | ||
| const dsData = (await dsBody.json()) as Record<string, unknown>; | ||
| const dataSourceUid = dsData.uid as string; | ||
|
|
||
| await backOff(async () => { | ||
| const { body, statusCode } = await grafanaRequest( | ||
| ctx, | ||
| 'POST', | ||
| '/api/ds/query', | ||
| { | ||
| queries: [ | ||
| { | ||
| datasource: { | ||
| type: 'grafana-amazonprometheus-datasource', | ||
| uid: dataSourceUid, | ||
| }, | ||
| expr: `{__name__=~"${ctx.config.ampNamespace}_.*"}`, | ||
| instant: true, | ||
| refId: 'A', | ||
| }, | ||
| ], | ||
| from: 'now-5m', | ||
| to: 'now', | ||
| }, | ||
| ); | ||
| assert.strictEqual(statusCode, 200, 'Expected query to succeed'); | ||
|
|
||
| const data = (await body.json()) as { | ||
| results: Record<string, { frames: Array<unknown> }>; | ||
| }; | ||
| const frames = data.results?.A?.frames ?? []; | ||
| assert.ok( | ||
| frames.length > 0, | ||
| `Expected Grafana to return metric frames for namespace '${ctx.config.ampNamespace}'`, | ||
| ); | ||
| }, backOffConfig); | ||
| }); | ||
|
|
||
| it('should have created the IAM role with AMP inline policy', async () => { | ||
| const iamRole = ctx.outputs!.ampGrafana.connections[0].role; | ||
| const grafanaAmpRoleArn = iamRole.arn as unknown as Unwrap< | ||
| typeof iamRole.arn | ||
| >; | ||
| const roleName = grafanaAmpRoleArn.split('/').pop()!; | ||
| const { Role } = await ctx.clients.iam.send( | ||
| new GetRoleCommand({ RoleName: roleName }), | ||
| ); | ||
| assert.ok(Role, 'Grafana IAM role should exist'); | ||
|
|
||
| const { PolicyNames } = await ctx.clients.iam.send( | ||
| new ListRolePoliciesCommand({ RoleName: roleName }), | ||
| ); | ||
| assert.ok( | ||
| PolicyNames && PolicyNames.length > 0, | ||
| 'IAM role should have at least one inline policy', | ||
| ); | ||
|
|
||
| const { PolicyDocument } = await ctx.clients.iam.send( | ||
| new GetRolePolicyCommand({ | ||
| RoleName: roleName, | ||
| PolicyName: PolicyNames[0], | ||
| }), | ||
| ); | ||
| const policy = JSON.parse(decodeURIComponent(PolicyDocument!)) as { | ||
| Statement: Array<{ Action: string[] }>; | ||
| }; | ||
| const actions = policy.Statement.flatMap(s => s.Action).sort(); | ||
| const expectedActions = [ | ||
| 'aps:GetSeries', | ||
| 'aps:GetLabels', | ||
| 'aps:GetMetricMetadata', | ||
| 'aps:QueryMetrics', | ||
| ].sort(); | ||
| assert.deepStrictEqual( | ||
| actions, | ||
| expectedActions, | ||
| 'AMP policy actions do not match expected actions', | ||
| ); | ||
| }); | ||
| } |
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,66 @@ | ||
| import { it } from 'node:test'; | ||
| import * as assert from 'node:assert'; | ||
| import * as studion from '@studion/infra-code-blocks'; | ||
| import { | ||
| GetRoleCommand, | ||
| GetRolePolicyCommand, | ||
| ListRolePoliciesCommand, | ||
| } from '@aws-sdk/client-iam'; | ||
| import { Unwrap } from '@pulumi/pulumi'; | ||
| import { backOff } from '../util'; | ||
| import { GrafanaTestContext } from './test-context'; | ||
| import { grafanaRequest } from './util'; | ||
|
|
||
| const backOffConfig = { numOfAttempts: 15 }; | ||
|
|
||
| export function testConfigurableGrafana(ctx: GrafanaTestContext) { | ||
| it('should have created the folder with the configured name', async () => { | ||
| const folder = ctx.outputs!.configurableGrafanaComponent.folder; | ||
| const folderUid = folder.uid as unknown as Unwrap<typeof folder.uid>; | ||
|
|
||
| await backOff(async () => { | ||
| const { body, statusCode } = await grafanaRequest( | ||
| ctx, | ||
| 'GET', | ||
| `/api/folders/${folderUid}`, | ||
| ); | ||
| assert.strictEqual(statusCode, 200, 'Expected folder to exist'); | ||
|
|
||
| const data = (await body.json()) as { title: string }; | ||
| assert.strictEqual( | ||
| data.title, | ||
| 'ICB Configurable Test Folder', | ||
| 'Expected folder title to match withFolderName() value', | ||
| ); | ||
| }, backOffConfig); | ||
| }); | ||
|
|
||
| it('should have created the custom dashboard', async () => { | ||
| const dashboard = ctx.outputs!.configurableGrafanaComponent.dashboards[0]; | ||
| const dashboardUid = dashboard.uid as unknown as Unwrap< | ||
| typeof dashboard.uid | ||
| >; | ||
|
|
||
| await backOff(async () => { | ||
| const { body, statusCode } = await grafanaRequest( | ||
| ctx, | ||
| 'GET', | ||
| `/api/dashboards/uid/${dashboardUid}`, | ||
| ); | ||
| assert.strictEqual(statusCode, 200, 'Expected custom dashboard to exist'); | ||
|
|
||
| const data = (await body.json()) as { | ||
| dashboard: { title: string; panels: Array<{ title: string }> }; | ||
| }; | ||
| assert.strictEqual( | ||
| data.dashboard.title, | ||
| 'ICB Grafana Configurable Dashboard', | ||
| 'Expected custom dashboard title', | ||
| ); | ||
| assert.ok( | ||
| data.dashboard.panels.length > 0, | ||
| 'Expected at least one panel', | ||
| ); | ||
| }, backOffConfig); | ||
| }); | ||
| } |
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,42 @@ | ||
| import { before, describe, after } from 'node:test'; | ||
| import { InlineProgramArgs, OutputMap } from '@pulumi/pulumi/automation'; | ||
| import { IAMClient } from '@aws-sdk/client-iam'; | ||
| import * as automation from '../automation'; | ||
| import { requireEnv, unwrapOutputs } from '../util'; | ||
| import { testAmpGrafana } from './amp-grafana.test'; | ||
| import { testConfigurableGrafana } from './configurable-grafana.test'; | ||
| import * as infraConfig from './infrastructure/config'; | ||
| import { GrafanaTestContext, ProgramOutput } from './test-context'; | ||
|
|
||
| const programArgs: InlineProgramArgs = { | ||
| stackName: 'dev', | ||
| projectName: 'icb-test-grafana', | ||
| program: () => import('./infrastructure'), | ||
| }; | ||
|
|
||
| const region = requireEnv('AWS_REGION'); | ||
| const ctx: GrafanaTestContext = { | ||
| config: { | ||
| region, | ||
| usersPath: infraConfig.usersPath, | ||
| appName: infraConfig.appName, | ||
| ampNamespace: infraConfig.ampNamespace, | ||
| grafanaUrl: requireEnv('GRAFANA_URL'), | ||
| grafanaAuth: requireEnv('GRAFANA_AUTH'), | ||
| }, | ||
| clients: { | ||
| iam: new IAMClient({ region }), | ||
| }, | ||
| }; | ||
|
|
||
| describe('Grafana component deployment', () => { | ||
| before(async () => { | ||
| const outputs: OutputMap = await automation.deploy(programArgs); | ||
| ctx.outputs = unwrapOutputs<ProgramOutput>(outputs); | ||
| }); | ||
|
|
||
| after(() => automation.destroy(programArgs)); | ||
|
|
||
| describe('AMP Grafana', () => testAmpGrafana(ctx)); | ||
| describe('Configurable Grafana', () => testConfigurableGrafana(ctx)); | ||
| }); | ||
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,11 @@ | ||
| export const appName = 'grafana-test'; | ||
|
|
||
| export const appImage = 'studiondev/observability-sample-app'; | ||
|
|
||
| export const appPort = 3000; | ||
bornast marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| export const usersPath = '/users'; | ||
|
|
||
| export const ampNamespace = 'icb_grafana_integration'; | ||
|
|
||
| export const apiFilter = 'http_route=~"/.*"'; | ||
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should other required env variables be explicitly required at this stage? Or can they be omitted?