Skip to content

Commit f5cbda1

Browse files
TackAdamAdam Tackett
andauthored
APM Cypress Testing + Prometheus (#2629)
* cypress testing apm Signed-off-by: Adam Tackett <tackadam@amazon.com> * update mapping Signed-off-by: Adam Tackett <tackadam@amazon.com> * fix datasource creation Signed-off-by: Adam Tackett <tackadam@amazon.com> * fix metrics check Signed-off-by: Adam Tackett <tackadam@amazon.com> * test check Signed-off-by: Adam Tackett <tackadam@amazon.com> * update metrics format Signed-off-by: Adam Tackett <tackadam@amazon.com> * fix commands Signed-off-by: Adam Tackett <tackadam@amazon.com> * wait test Signed-off-by: Adam Tackett <tackadam@amazon.com> * debugging check Signed-off-by: Adam Tackett <tackadam@amazon.com> * update sample data with remote service Signed-off-by: Adam Tackett <tackadam@amazon.com> * remove log Signed-off-by: Adam Tackett <tackadam@amazon.com> * fix index.js Signed-off-by: Adam Tackett <tackadam@amazon.com> * logging check Signed-off-by: Adam Tackett <tackadam@amazon.com> * add log task for check Signed-off-by: Adam Tackett <tackadam@amazon.com> * timerange check Signed-off-by: Adam Tackett <tackadam@amazon.com> * more logs Signed-off-by: Adam Tackett <tackadam@amazon.com> * fix timestamp calc for upload Signed-off-by: Adam Tackett <tackadam@amazon.com> * fix timestamp calc for upload Signed-off-by: Adam Tackett <tackadam@amazon.com> * fix timestamp calc for upload Signed-off-by: Adam Tackett <tackadam@amazon.com> * fix timestamps Signed-off-by: Adam Tackett <tackadam@amazon.com> * more logs, revert remoteservice on data Signed-off-by: Adam Tackett <tackadam@amazon.com> * try backfill script Signed-off-by: Adam Tackett <tackadam@amazon.com> * testing change Signed-off-by: Adam Tackett <tackadam@amazon.com> * testing change Signed-off-by: Adam Tackett <tackadam@amazon.com> * testing change Signed-off-by: Adam Tackett <tackadam@amazon.com> * testing change3 Signed-off-by: Adam Tackett <tackadam@amazon.com> * testing change4 Signed-off-by: Adam Tackett <tackadam@amazon.com> * testing change5 Signed-off-by: Adam Tackett <tackadam@amazon.com> * testing change6 Signed-off-by: Adam Tackett <tackadam@amazon.com> * testing remove backfill Signed-off-by: Adam Tackett <tackadam@amazon.com> * sunday test Signed-off-by: Adam Tackett <tackadam@amazon.com> * update to 3.6 back-end Signed-off-by: Adam Tackett <tackadam@amazon.com> * remove old code Signed-off-by: Adam Tackett <tackadam@amazon.com> * update command for fields Signed-off-by: Adam Tackett <tackadam@amazon.com> * fix span timestamps Signed-off-by: Adam Tackett <tackadam@amazon.com> * dynamic version from package.json Signed-off-by: Adam Tackett <tackadam@amazon.com> --------- Signed-off-by: Adam Tackett <tackadam@amazon.com> Co-authored-by: Adam Tackett <tackadam@amazon.com>
1 parent 6554100 commit f5cbda1

26 files changed

+69030
-29
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
global:
2+
scrape_interval: 5s # Fast scraping for test data
3+
evaluation_interval: 5s
4+
5+
# Scrape APM test metrics from the metrics server
6+
scrape_configs:
7+
- job_name: "apm_test_data"
8+
static_configs:
9+
- targets: ["localhost:8080"]
10+
scrape_interval: 5s
11+
scrape_timeout: 4s
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
/// <reference types="cypress" />
7+
8+
import { PROMETHEUS_CLUSTER } from '../../utils/constants';
9+
import { uploadAPMDataToOpenSearch, waitForPrometheusMetrics, verifyPrometheusReady, getAPMTestTimeRange } from '../../utils/apm_data_helpers';
10+
import { setupAPMTestEnvironment, cleanupObservabilityWorkspace } from '../../utils/helpers';
11+
import { getRandomizedWorkspaceName, getRandomizedDatasetId, formatDateForPicker } from '../../utils/shared';
12+
13+
const workspaceName = getRandomizedWorkspaceName('apm-services');
14+
const traceDatasetId = getRandomizedDatasetId('trace');
15+
const serviceDatasetId = getRandomizedDatasetId('service');
16+
const logDatasetId = getRandomizedDatasetId('log');
17+
18+
const APM_RESOURCES = {
19+
DATASOURCE_NAME: Cypress.env('dataSourceTitle') || 'default',
20+
DATA_CONNECTION_NAME: 'prom_integ_test',
21+
TRACE_INDEX_PATTERN: 'otel_v1_apm_span_explore',
22+
TRACE_TIME_FIELD: 'endTime',
23+
SERVICE_INDEX_PATTERN: 'otel_apm_service_map_explore',
24+
SERVICE_TIME_FIELD: 'timestamp',
25+
LOG_INDEX_PATTERN: 'logs_otel_v1_explore',
26+
LOG_TIME_FIELD: 'time',
27+
};
28+
29+
const setAPMTimeRange = (startDate, endDate) => {
30+
const opts = { log: false };
31+
32+
// Close any open popovers first
33+
cy.get('body', opts).then(($body) => {
34+
if ($body.find('[data-test-subj="superDatePickerAbsoluteTab"]').length > 0) {
35+
cy.get('body').type('{esc}', opts);
36+
}
37+
});
38+
39+
// Find and click the date picker button
40+
cy.getElementsByTestIds(
41+
['superDatePickerstartDatePopoverButton', 'superDatePickerShowDatesButton'],
42+
opts
43+
)
44+
.should('be.visible')
45+
.invoke('attr', 'data-test-subj')
46+
.then((testId) => {
47+
cy.getElementByTestId(testId, opts).should('be.visible').click(opts);
48+
});
49+
50+
// Ensure date selection dialog is open
51+
cy.whenTestIdNotFound('superDatePickerAbsoluteTab', () => {
52+
cy.getElementByTestId('superDatePickerstartDatePopoverButton', opts)
53+
.should('be.visible')
54+
.click(opts);
55+
});
56+
57+
// Set start date
58+
cy.getElementByTestId('superDatePickerAbsoluteTab', opts).first(opts).click(opts);
59+
cy.getElementByTestId('superDatePickerAbsoluteDateInput', opts)
60+
.first(opts)
61+
.click(opts)
62+
.clear(opts)
63+
.type(startDate, { ...opts, delay: 0 });
64+
65+
// Set end date
66+
cy.getElementByTestId('superDatePickerendDatePopoverButton', opts).last(opts).click(opts);
67+
cy.getElementByTestId('superDatePickerAbsoluteTab', opts).last(opts).click(opts);
68+
cy.getElementByTestId('superDatePickerAbsoluteDateInput', opts)
69+
.last(opts)
70+
.click(opts)
71+
.clear(opts)
72+
.type(endDate, { ...opts, delay: 0 });
73+
74+
// Close popup
75+
cy.getElementByTestId('superDatePickerendDatePopoverButton', opts).click(opts);
76+
77+
// Click Apply button
78+
cy.get('[data-test-subj="superDatePickerApplyTimeButton"]').should('be.visible').click();
79+
cy.get('[data-test-subj="globalLoadingIndicator"]', { timeout: 60000 }).should('not.exist');
80+
};
81+
82+
describe('APM Services Page', () => {
83+
const prometheusConfig = PROMETHEUS_CLUSTER;
84+
85+
let workspaceId;
86+
let startTime;
87+
let endTime;
88+
89+
before(() => {
90+
if (!prometheusConfig.url) {
91+
throw new Error(
92+
'APM tests require Prometheus. Set PROMETHEUS_CONNECTION_URL environment variable.'
93+
);
94+
}
95+
96+
// Chain all async operations with return to ensure proper sequencing
97+
return getAPMTestTimeRange()
98+
.then((timeRange) => {
99+
// Calculate time range for current time window
100+
startTime = formatDateForPicker(timeRange.start);
101+
endTime = formatDateForPicker(timeRange.end);
102+
})
103+
.then(() => {
104+
// Upload raw data to OpenSearch indices
105+
return uploadAPMDataToOpenSearch();
106+
})
107+
.then(() => {
108+
// Wait for Prometheus to be ready with scraped metrics
109+
return waitForPrometheusMetrics(prometheusConfig.url);
110+
})
111+
.then(() => {
112+
// Setup APM test environment with workspace, datasets, and Prometheus
113+
return setupAPMTestEnvironment({
114+
datasourceName: APM_RESOURCES.DATASOURCE_NAME,
115+
workspaceName: workspaceName,
116+
prometheusConnectionName: APM_RESOURCES.DATA_CONNECTION_NAME,
117+
prometheusUrl: prometheusConfig.url,
118+
datasets: {
119+
trace: {
120+
id: traceDatasetId,
121+
config: {
122+
title: APM_RESOURCES.TRACE_INDEX_PATTERN,
123+
signalType: 'traces',
124+
timestamp: APM_RESOURCES.TRACE_TIME_FIELD,
125+
},
126+
},
127+
service: {
128+
id: serviceDatasetId,
129+
config: {
130+
title: APM_RESOURCES.SERVICE_INDEX_PATTERN,
131+
signalType: 'logs',
132+
timestamp: APM_RESOURCES.SERVICE_TIME_FIELD,
133+
},
134+
},
135+
log: {
136+
id: logDatasetId,
137+
config: {
138+
title: APM_RESOURCES.LOG_INDEX_PATTERN,
139+
signalType: 'logs',
140+
timestamp: APM_RESOURCES.LOG_TIME_FIELD,
141+
},
142+
},
143+
},
144+
}).then((wsId) => {
145+
workspaceId = wsId;
146+
});
147+
});
148+
});
149+
150+
after(() => {
151+
cleanupObservabilityWorkspace(workspaceName);
152+
});
153+
154+
describe('APM Configuration and Display', () => {
155+
beforeEach(() => {
156+
// Verify Prometheus is healthy before loading the page
157+
verifyPrometheusReady(prometheusConfig.url);
158+
159+
// Navigate to APM Services page in the workspace
160+
cy.visit(`/w/${workspaceId}/app/observability-apm-services`, {
161+
onBeforeLoad: (win) => {
162+
win.sessionStorage.clear();
163+
},
164+
});
165+
cy.get('[data-test-subj="globalLoadingIndicator"]').should('not.exist');
166+
});
167+
168+
it('should configure APM settings and display services page', () => {
169+
// Click "Get started" button to open settings modal
170+
cy.contains('button', 'Get started').should('be.visible').click();
171+
172+
// Wait for modal to appear
173+
cy.get('.euiModal').should('be.visible');
174+
cy.get('.euiModalHeader').should('be.visible');
175+
176+
// Select Traces dataset
177+
cy.get('.euiFormRow')
178+
.contains('Traces')
179+
.parent()
180+
.parent()
181+
.find('.euiComboBox')
182+
.click();
183+
cy.get('.euiComboBoxOptionsList').should('be.visible');
184+
cy.contains(APM_RESOURCES.TRACE_INDEX_PATTERN).click();
185+
cy.get('.euiComboBoxOptionsList').should('not.exist');
186+
187+
// Select Services dataset
188+
cy.get('.euiFormRow')
189+
.contains('Services')
190+
.parent()
191+
.parent()
192+
.find('.euiComboBox')
193+
.click();
194+
cy.get('.euiComboBoxOptionsList').should('be.visible');
195+
cy.contains(APM_RESOURCES.SERVICE_INDEX_PATTERN).click();
196+
cy.get('.euiComboBoxOptionsList').should('not.exist');
197+
198+
// Select Prometheus data source
199+
cy.get('.euiFormRow')
200+
.contains('RED Metrics')
201+
.parent()
202+
.parent()
203+
.find('.euiComboBox')
204+
.click();
205+
cy.get('.euiComboBoxOptionsList').should('be.visible');
206+
cy.contains(APM_RESOURCES.DATA_CONNECTION_NAME).click();
207+
cy.get('.euiComboBoxOptionsList').should('not.exist');
208+
209+
// Click Apply button
210+
cy.get('.euiModalFooter').find('.euiButton--fill').click();
211+
212+
// Wait for modal to close and page to reload
213+
cy.get('.euiModal').should('not.exist');
214+
cy.get('[data-test-subj="globalLoadingIndicator"]').should('not.exist');
215+
216+
// Set up intercepts BEFORE setting time range to capture all queries
217+
cy.intercept('POST', '**/api/enhancements/search/promql').as('promqlCall');
218+
cy.intercept('POST', '**/api/enhancements/search/ppl').as('pplCall');
219+
220+
// Set time range - this triggers widget reloads with correct time range
221+
setAPMTimeRange(startTime, endTime);
222+
223+
// Wait for loading to complete after time range change
224+
cy.get('[data-test-subj="globalLoadingIndicator"]', { timeout: 10000 }).should('not.exist');
225+
226+
// Verify page loaded successfully with service data
227+
// Look for specific services from the test data
228+
cy.get('body').should('contain', 'cart');
229+
});
230+
231+
it('should navigate to Application Map page', () => {
232+
cy.visit(`/w/${workspaceId}/app/observability-apm-application-map`, {
233+
onBeforeLoad: (win) => {
234+
win.sessionStorage.clear();
235+
},
236+
});
237+
cy.get('[data-test-subj="globalLoadingIndicator"]').should('not.exist');
238+
239+
// Set time range
240+
setAPMTimeRange(startTime, endTime);
241+
242+
// Verify Application Map page loaded
243+
cy.get('[data-test-subj="applicationMapPage"]', { timeout: 30000 }).should('be.visible');
244+
cy.contains('View insights').should('be.visible');
245+
});
246+
});
247+
});

.cypress/support/commands.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,20 @@ Cypress.Commands.overwrite('request', (originalFn, ...args) => {
6969

7070
return originalFn(Object.assign({}, defaults, options));
7171
});
72+
73+
// Custom commands for finding elements by test ID
74+
Cypress.Commands.add('getElementByTestId', (testId, options = {}) => {
75+
return cy.get(`[data-test-subj="${testId}"]`, options);
76+
});
77+
78+
Cypress.Commands.add('getElementsByTestIds', (testIds, options = {}) => {
79+
const selectors = [testIds].flat(Infinity).map((testId) => `[data-test-subj="${testId}"]`);
80+
return cy.get(selectors.join(','), options);
81+
});
82+
83+
Cypress.Commands.add('whenTestIdNotFound', (testIds, callbackFn, options = {}) => {
84+
const selectors = [testIds].flat(Infinity).map((testId) => `[data-test-subj="${testId}"]`);
85+
cy.get('body', options).then(($body) => {
86+
if ($body.find(selectors.join(',')).length === 0) callbackFn();
87+
});
88+
});

.cypress/support/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import './commands';
2626
// Alternatively you can use CommonJS syntax:
2727
// require('./commands')
2828

29+
// Import OSD commands for workspace/dataset management (used by APM tests)
30+
import '../utils/commands.osd';
31+
2932
// Switch the base URL of OpenSearch when security enabled in the cluster
3033
if (Cypress.env('security_enabled')) {
3134
Cypress.env('opensearch', 'https://localhost:9200');

0 commit comments

Comments
 (0)