Skip to content

Commit 7b69e2d

Browse files
upcoming: [DI-29907] - Logs service Alerts Integration (#13445)
* upcoming: [DI-29907] - Logs service Alerts Integration * add mocks for easier testing * add changesets * fix lint issues * fix for failing cypress test: adding scrollIntoView to the clickable component
1 parent a66dd2f commit 7b69e2d

13 files changed

Lines changed: 257 additions & 29 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/api-v4": Upcoming Features
3+
---
4+
5+
Add logs to `CloudPulseServiceType` and `capabilityServiceTypeMapping` ([#13445](https://github.com/linode/manager/pull/13445))

packages/api-v4/src/cloudpulse/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type CloudPulseServiceType =
99
| 'firewall'
1010
| 'linode'
1111
| 'lke'
12+
| 'logs'
1213
| 'netloadbalancer'
1314
| 'nodebalancer'
1415
| 'objectstorage';
@@ -429,6 +430,7 @@ export const capabilityServiceTypeMapping: Record<
429430
blockstorage: 'Block Storage',
430431
lke: 'Kubernetes',
431432
netloadbalancer: 'Network LoadBalancer',
433+
logs: 'Akamai Cloud Pulse Logs',
432434
};
433435

434436
/**
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Upcoming Features
3+
---
4+
5+
Integrate aclp-logs service to alerts with custom validation schemas, error texts ([#13445](https://github.com/linode/manager/pull/13445))

packages/manager/cypress/e2e/core/cloudpulse/alerting-notification-channel-permission-tests.spec.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ describe('Notification Channel Listing Page — Access Control', () => {
3737
mockAppendFeatureFlags(flags);
3838
cy.visitWithLogin('/linodes');
3939

40-
ui.nav.findItemByTitle('Alerts').should('be.visible').click();
40+
ui.nav.findItemByTitle('Alerts').as('alertsNav');
41+
cy.get('@alertsNav').scrollIntoView();
42+
cy.get('@alertsNav').should('be.visible').click();
43+
4144
ui.tabList
4245
.findTabByTitle('Notification Channels')
4346
.should('be.visible')
@@ -62,7 +65,9 @@ describe('Notification Channel Listing Page — Access Control', () => {
6265
mockAppendFeatureFlags(flags);
6366
cy.visitWithLogin('/linodes');
6467

65-
ui.nav.findItemByTitle('Alerts').should('be.visible').click();
68+
ui.nav.findItemByTitle('Alerts').as('alertsNav');
69+
cy.get('@alertsNav').scrollIntoView();
70+
cy.get('@alertsNav').should('be.visible').click();
6671

6772
// Tab should not render at all
6873
ui.tabList.findTabByTitle('Notification Channels').should('not.exist');

packages/manager/src/factories/cloudpulse/alerts.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,3 +731,68 @@ export const networkLoadBalancerMetricCriteria: MetricDefinition[] = [
731731
],
732732
},
733733
];
734+
735+
const logsDimensions: Dimension[] = [
736+
{
737+
label: 'Status Code',
738+
dimension_label: 'status_code',
739+
values: [],
740+
},
741+
];
742+
743+
export const logsMetricCriteria: MetricDefinition[] = [
744+
{
745+
label: 'Successful Upload Count',
746+
metric: 'success_upload_count',
747+
unit: 'Count',
748+
scrape_interval: '300s',
749+
metric_type: 'gauge',
750+
is_alertable: true,
751+
available_aggregate_functions: ['sum'],
752+
dimensions: logsDimensions,
753+
},
754+
{
755+
label: 'Error Upload Count',
756+
metric: 'error_upload_count',
757+
unit: 'Count',
758+
scrape_interval: '300s',
759+
metric_type: 'gauge',
760+
is_alertable: true,
761+
available_aggregate_functions: ['sum'],
762+
dimensions: logsDimensions,
763+
},
764+
{
765+
label: 'Error Upload Rate',
766+
metric: 'error_upload_rate',
767+
unit: 'Percent',
768+
scrape_interval: '300s',
769+
metric_type: 'gauge',
770+
is_alertable: true,
771+
available_aggregate_functions: ['avg'],
772+
dimensions: logsDimensions,
773+
},
774+
];
775+
776+
export const logsAlertMetricCriteria =
777+
Factory.Sync.makeFactory<AlertDefinitionMetricCriteria>({
778+
label: 'Successful Upload Count',
779+
metric: 'success_upload_count',
780+
unit: 'Count',
781+
aggregate_function: 'sum',
782+
operator: 'eq',
783+
threshold: 1500,
784+
dimension_filters: [
785+
{
786+
label: 'Status Code',
787+
dimension_label: 'status_code',
788+
operator: 'in',
789+
value: '203,402',
790+
},
791+
{
792+
label: 'Status Code',
793+
dimension_label: 'status_code',
794+
operator: 'eq',
795+
value: '503',
796+
},
797+
],
798+
});

packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,8 @@ export const AlertResources = React.memo((props: AlertResourcesProp) => {
179179
const filteredTypes =
180180
alertClass === 'shared'
181181
? Object.keys(databaseTypeClassMap).filter(
182-
(type) => type !== 'dedicated'
183-
)
182+
(type) => type !== 'dedicated'
183+
)
184184
: [alertClass];
185185

186186
// Apply type filter only for DBaaS user alerts with a valid alertClass based on above filtered types
@@ -205,7 +205,10 @@ export const AlertResources = React.memo((props: AlertResourcesProp) => {
205205
isLoading: isResourcesLoading,
206206
} = useResourcesQuery(
207207
Boolean(
208-
serviceType && (serviceType === 'firewall' || supportedRegionIds?.length)
208+
serviceType &&
209+
(serviceType === 'firewall' ||
210+
serviceType === 'logs' ||
211+
supportedRegionIds?.length)
209212
), // Enable query only if serviceType and supportedRegionIds are available, in case of firewall only serviceType is needed
210213
serviceType,
211214
{},
@@ -468,8 +471,8 @@ export const AlertResources = React.memo((props: AlertResourcesProp) => {
468471
new Set(
469472
regionFilteredResources
470473
? regionFilteredResources.flatMap(
471-
({ tags }) => tags ?? []
472-
)
474+
({ tags }) => tags ?? []
475+
)
473476
: []
474477
)
475478
),

packages/manager/src/features/CloudPulse/Alerts/AlertsResources/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ export const serviceTypeBasedColumns: ServiceColumns<AlertInstance> = {
116116
sortingKey: 'region',
117117
},
118118
],
119+
logs: [
120+
{
121+
accessor: ({ label }) => label,
122+
label: 'Entity',
123+
sortingKey: 'label',
124+
},
125+
],
119126
};
120127

121128
export const serviceToFiltersMap: Partial<
@@ -138,6 +145,7 @@ export const serviceToFiltersMap: Partial<
138145
{ component: AlertsEndpointFilter, filterKey: 'endpoint' },
139146
],
140147
blockstorage: [{ component: AlertsRegionFilter, filterKey: 'region' }],
148+
logs: [],
141149
};
142150
export const applicableAdditionalFilterKeys: AlertAdditionalFilterKey[] = [
143151
'engineType', // Extendable in future for filter keys like 'tags', 'plan', etc.

packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterValue/ValueSchemas.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import {
1919
INTERFACE_ID_ERROR_MESSAGE,
2020
PORT_HELPER_TEXT,
2121
PORTS_TRAILING_COMMA_ERROR_MESSAGE,
22+
STATUS_CODE_ERROR_MESSAGE,
23+
STATUS_CODES_ERROR_MESSAGE,
24+
STATUS_CODES_HELPER_TEXT,
2225
} from '../../../constants';
2326

2427
const LENGTH_ERROR_MESSAGE = 'Value must be 100 characters or less.';
@@ -240,6 +243,73 @@ const multipleInterfacesSchema = string()
240243
}
241244
);
242245

246+
const singleStatusCodeSchema = string()
247+
.max(100, LENGTH_ERROR_MESSAGE)
248+
.test(
249+
'validate-single-status-code-schema',
250+
STATUS_CODE_ERROR_MESSAGE,
251+
function (value) {
252+
if (!value || typeof value !== 'string') {
253+
return this.createError({ message: fieldErrorMessage });
254+
}
255+
256+
if (!CONFIG_NUMBER_REGEX.test(value)) {
257+
return this.createError({ message: STATUS_CODE_ERROR_MESSAGE });
258+
}
259+
260+
return true;
261+
}
262+
);
263+
264+
const multipleStatusCodeSchema = string()
265+
.max(100, LENGTH_ERROR_MESSAGE)
266+
.test(
267+
'validate-multi-status-code-schema',
268+
STATUS_CODES_ERROR_MESSAGE,
269+
function (value) {
270+
if (!value || typeof value !== 'string') {
271+
return this.createError({ message: fieldErrorMessage });
272+
}
273+
if (value.includes(' ')) {
274+
return this.createError({ message: STATUS_CODES_ERROR_MESSAGE });
275+
}
276+
277+
if (value.trim().endsWith(',')) {
278+
return this.createError({
279+
message: PORTS_TRAILING_COMMA_ERROR_MESSAGE,
280+
});
281+
}
282+
283+
if (value.trim().startsWith(',')) {
284+
return this.createError({ message: PORTS_LEADING_COMMA_ERROR_MESSAGE });
285+
}
286+
287+
if (value.trim().includes(',,')) {
288+
return this.createError({
289+
message: CONFIG_IDS_CONSECUTIVE_COMMAS_ERROR_MESSAGE,
290+
});
291+
}
292+
if (value.includes('.')) {
293+
return this.createError({ message: STATUS_CODES_HELPER_TEXT });
294+
}
295+
296+
const rawSegments = value.split(',');
297+
// Check for empty segments
298+
if (rawSegments.some((segment) => segment.trim() === '')) {
299+
return this.createError({
300+
message: CONFIG_IDS_CONSECUTIVE_COMMAS_ERROR_MESSAGE,
301+
});
302+
}
303+
for (const configId of rawSegments) {
304+
const trimmedConfigId = configId.trim();
305+
306+
if (!CONFIG_NUMBER_REGEX.test(trimmedConfigId)) {
307+
return this.createError({ message: STATUS_CODE_ERROR_MESSAGE });
308+
}
309+
}
310+
return true;
311+
}
312+
);
243313
const baseValueSchema = string()
244314
.nullable()
245315
.required(fieldErrorMessage)
@@ -269,6 +339,11 @@ export const getDimensionFilterValueSchema = ({
269339
operator === 'in' ? multipleInterfacesSchema : singleInterfaceSchema;
270340
return interfaceSchema.concat(baseValueSchema);
271341
}
342+
if (dimensionLabel === 'status_code') {
343+
const statusCodeSchema =
344+
operator === 'in' ? multipleStatusCodeSchema : singleStatusCodeSchema;
345+
return statusCodeSchema.concat(baseValueSchema);
346+
}
272347
if (['endswith', 'startswith'].includes(operator)) {
273348
return string().max(100, LENGTH_ERROR_MESSAGE).concat(baseValueSchema);
274349
}

packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterValue/constants.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import {
1515
PORT_HELPER_TEXT,
1616
PORT_PLACEHOLDER_TEXT,
1717
PORTS_PLACEHOLDER_TEXT,
18+
STATUS_CODE_HELPER_TEXT,
19+
STATUS_CODE_PLACEHOLDER_TEXT,
20+
STATUS_CODES_HELPER_TEXT,
21+
STATUS_CODES_PLACEHOLDER_TEXT,
1822
VIP_HELPER_TEXT,
1923
VIP_PLACEHOLDER_TEXT,
2024
} from '../../../constants';
@@ -366,6 +370,34 @@ export const valueFieldConfig: ValueFieldConfigMap = {
366370
inputType: 'text',
367371
},
368372
},
373+
status_code: {
374+
eq_neq: {
375+
type: 'textfield',
376+
inputType: 'number',
377+
min: 0,
378+
max: Number.MAX_SAFE_INTEGER,
379+
placeholder: STATUS_CODE_PLACEHOLDER_TEXT,
380+
helperText: STATUS_CODE_HELPER_TEXT,
381+
},
382+
startswith_endswith: {
383+
type: 'textfield',
384+
inputType: 'number',
385+
min: 0,
386+
max: Number.MAX_SAFE_INTEGER,
387+
placeholder: STATUS_CODE_PLACEHOLDER_TEXT,
388+
helperText: STATUS_CODE_HELPER_TEXT,
389+
},
390+
in: {
391+
type: 'textfield',
392+
inputType: 'text',
393+
placeholder: STATUS_CODES_PLACEHOLDER_TEXT,
394+
helperText: STATUS_CODES_HELPER_TEXT,
395+
},
396+
'*': {
397+
type: 'textfield',
398+
inputType: 'number',
399+
},
400+
},
369401
emptyValue: {
370402
eq_neq: {
371403
type: 'textfield',

packages/manager/src/features/CloudPulse/Alerts/constants.ts

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import type { FieldPath } from 'react-hook-form';
22

3-
import { PORTS_HELPER_TEXT } from '../Utils/constants';
4-
53
import type { CreateAlertDefinitionForm } from './CreateAlert/types';
64
import type {
75
AlertDefinitionScope,
@@ -277,27 +275,17 @@ export const CONFIGS_ID_PLACEHOLDER_TEXT = 'e.g., 1234,5678';
277275

278276
export const INTERFACE_ID_ERROR_MESSAGE = 'Enter a valid interface ID number.';
279277
export const INTERFACE_ID_HELPER_TEXT = 'Enter an interface ID number.';
280-
export const PLACEHOLDER_TEXT_MAP: Record<string, Record<string, string>> = {
281-
port: {
282-
in: PORTS_PLACEHOLDER_TEXT,
283-
default: PORT_PLACEHOLDER_TEXT,
284-
},
285-
config_id: {
286-
in: CONFIGS_ID_PLACEHOLDER_TEXT,
287-
default: CONFIG_ID_PLACEHOLDER_TEXT,
288-
},
289-
};
290278

291-
export const HELPER_TEXT_MAP: Record<string, Record<string, string>> = {
292-
port: {
293-
in: PORTS_HELPER_TEXT,
294-
default: PORT_HELPER_TEXT,
295-
},
296-
config_id: {
297-
in: CONFIGS_HELPER_TEXT,
298-
default: CONFIG_ERROR_MESSAGE,
299-
},
300-
};
279+
export const STATUS_CODE_PLACEHOLDER_TEXT = 'e.g., 200';
280+
export const STATUS_CODES_PLACEHOLDER_TEXT = 'e.g., 200,403,500';
281+
282+
export const STATUS_CODE_HELPER_TEXT = 'Enter a status code number.';
283+
export const STATUS_CODES_HELPER_TEXT =
284+
'Enter one or more status codes separated by commas.';
285+
286+
export const STATUS_CODE_ERROR_MESSAGE = 'Enter a valid status code number.';
287+
export const STATUS_CODES_ERROR_MESSAGE =
288+
'Enter valid status codes as integers separated by commas.';
301289

302290
export const entityLabelMap = {
303291
linode: 'Linode',

0 commit comments

Comments
 (0)