From 1938b0f5c75aec9dd4f57637836b1aaf26e7b478 Mon Sep 17 00:00:00 2001 From: santoshp210-akamai <159890961+santoshp210-akamai@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:27:02 +0530 Subject: [PATCH 1/4] upcoming: [DI-29907] - Logs service Alerts Integration --- packages/api-v4/src/cloudpulse/types.ts | 2 + .../src/factories/cloudpulse/alerts.ts | 41 ++++++++++ .../AlertsResources/AlertsResources.tsx | 5 +- .../Alerts/AlertsResources/constants.ts | 8 ++ .../DimensionFilterValue/ValueSchemas.ts | 75 +++++++++++++++++++ .../DimensionFilterValue/constants.ts | 32 ++++++++ .../features/CloudPulse/Alerts/constants.ts | 32 +++----- .../src/features/CloudPulse/Utils/models.ts | 2 + packages/manager/src/mocks/serverHandlers.ts | 15 ++++ .../manager/src/queries/cloudpulse/queries.ts | 3 + 10 files changed, 192 insertions(+), 23 deletions(-) diff --git a/packages/api-v4/src/cloudpulse/types.ts b/packages/api-v4/src/cloudpulse/types.ts index 89e798853d5..e72bef9d0a7 100644 --- a/packages/api-v4/src/cloudpulse/types.ts +++ b/packages/api-v4/src/cloudpulse/types.ts @@ -9,6 +9,7 @@ export type CloudPulseServiceType = | 'firewall' | 'linode' | 'lke' + | 'logs' | 'netloadbalancer' | 'nodebalancer' | 'objectstorage'; @@ -429,6 +430,7 @@ export const capabilityServiceTypeMapping: Record< blockstorage: 'Block Storage', lke: 'Kubernetes', netloadbalancer: 'Network LoadBalancer', + logs: 'Akamai Cloud Pulse Logs', }; /** diff --git a/packages/manager/src/factories/cloudpulse/alerts.ts b/packages/manager/src/factories/cloudpulse/alerts.ts index 9f7a9881211..2a67abc55bc 100644 --- a/packages/manager/src/factories/cloudpulse/alerts.ts +++ b/packages/manager/src/factories/cloudpulse/alerts.ts @@ -731,3 +731,44 @@ export const networkLoadBalancerMetricCriteria: MetricDefinition[] = [ ], }, ]; + +const logsDimensions: Dimension[] = [ + { + label: 'Status Code', + dimension_label: 'status_code', + values: [], + }, +]; + +export const logsMetricCriteria: MetricDefinition[] = [ + { + label: 'Successful Upload Count', + metric: 'success_upload_count', + unit: 'Count', + scrape_interval: '300s', + metric_type: 'gauge', + is_alertable: true, + available_aggregate_functions: ['sum'], + dimensions: logsDimensions, + }, + { + label: 'Error Upload Count', + metric: 'error_upload_count', + unit: 'Count', + scrape_interval: '300s', + metric_type: 'gauge', + is_alertable: true, + available_aggregate_functions: ['sum'], + dimensions: logsDimensions, + }, + { + label: 'Error Upload Rate', + metric: 'error_upload_rate', + unit: 'Percent', + scrape_interval: '300s', + metric_type: 'gauge', + is_alertable: true, + available_aggregate_functions: ['avg'], + dimensions: logsDimensions, + }, +]; diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.tsx index 6207bf3eb6d..4d14cb16ad3 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.tsx @@ -205,7 +205,10 @@ export const AlertResources = React.memo((props: AlertResourcesProp) => { isLoading: isResourcesLoading, } = useResourcesQuery( Boolean( - serviceType && (serviceType === 'firewall' || supportedRegionIds?.length) + serviceType && + (serviceType === 'firewall' || + serviceType === 'logs' || + supportedRegionIds?.length) ), // Enable query only if serviceType and supportedRegionIds are available, in case of firewall only serviceType is needed serviceType, {}, diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/constants.ts b/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/constants.ts index 57a793cc4b2..b5254d736a3 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/constants.ts +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/constants.ts @@ -116,6 +116,13 @@ export const serviceTypeBasedColumns: ServiceColumns = { sortingKey: 'region', }, ], + logs: [ + { + accessor: ({ label }) => label, + label: 'Entity', + sortingKey: 'label', + }, + ], }; export const serviceToFiltersMap: Partial< @@ -138,6 +145,7 @@ export const serviceToFiltersMap: Partial< { component: AlertsEndpointFilter, filterKey: 'endpoint' }, ], blockstorage: [{ component: AlertsRegionFilter, filterKey: 'region' }], + logs: [], }; export const applicableAdditionalFilterKeys: AlertAdditionalFilterKey[] = [ 'engineType', // Extendable in future for filter keys like 'tags', 'plan', etc. diff --git a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterValue/ValueSchemas.ts b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterValue/ValueSchemas.ts index 89e705be80b..c3e9dd3ed2a 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterValue/ValueSchemas.ts +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterValue/ValueSchemas.ts @@ -19,6 +19,9 @@ import { INTERFACE_ID_ERROR_MESSAGE, PORT_HELPER_TEXT, PORTS_TRAILING_COMMA_ERROR_MESSAGE, + STATUS_CODE_ERROR_MESSAGE, + STATUS_CODES_ERROR_MESSAGE, + STATUS_CODES_HELPER_TEXT, } from '../../../constants'; const LENGTH_ERROR_MESSAGE = 'Value must be 100 characters or less.'; @@ -240,6 +243,73 @@ const multipleInterfacesSchema = string() } ); +const singleStatusCodeSchema = string() + .max(100, LENGTH_ERROR_MESSAGE) + .test( + 'validate-single-status-code-schema', + STATUS_CODE_ERROR_MESSAGE, + function (value) { + if (!value || typeof value !== 'string') { + return this.createError({ message: fieldErrorMessage }); + } + + if (!CONFIG_NUMBER_REGEX.test(value)) { + return this.createError({ message: STATUS_CODE_ERROR_MESSAGE }); + } + + return true; + } + ); + +const multipleStatusCodeSchema = string() + .max(100, LENGTH_ERROR_MESSAGE) + .test( + 'validate-multi-status-code-schema', + STATUS_CODES_ERROR_MESSAGE, + function (value) { + if (!value || typeof value !== 'string') { + return this.createError({ message: fieldErrorMessage }); + } + if (value.includes(' ')) { + return this.createError({ message: STATUS_CODES_ERROR_MESSAGE }); + } + + if (value.trim().endsWith(',')) { + return this.createError({ + message: PORTS_TRAILING_COMMA_ERROR_MESSAGE, + }); + } + + if (value.trim().startsWith(',')) { + return this.createError({ message: PORTS_LEADING_COMMA_ERROR_MESSAGE }); + } + + if (value.trim().includes(',,')) { + return this.createError({ + message: CONFIG_IDS_CONSECUTIVE_COMMAS_ERROR_MESSAGE, + }); + } + if (value.includes('.')) { + return this.createError({ message: STATUS_CODES_HELPER_TEXT }); + } + + const rawSegments = value.split(','); + // Check for empty segments + if (rawSegments.some((segment) => segment.trim() === '')) { + return this.createError({ + message: CONFIG_IDS_CONSECUTIVE_COMMAS_ERROR_MESSAGE, + }); + } + for (const configId of rawSegments) { + const trimmedConfigId = configId.trim(); + + if (!CONFIG_NUMBER_REGEX.test(trimmedConfigId)) { + return this.createError({ message: STATUS_CODE_ERROR_MESSAGE }); + } + } + return true; + } + ); const baseValueSchema = string() .nullable() .required(fieldErrorMessage) @@ -269,6 +339,11 @@ export const getDimensionFilterValueSchema = ({ operator === 'in' ? multipleInterfacesSchema : singleInterfaceSchema; return interfaceSchema.concat(baseValueSchema); } + if (dimensionLabel === 'status_code') { + const statusCodeSchema = + operator === 'in' ? multipleStatusCodeSchema : singleStatusCodeSchema; + return statusCodeSchema.concat(baseValueSchema); + } if (['endswith', 'startswith'].includes(operator)) { return string().max(100, LENGTH_ERROR_MESSAGE).concat(baseValueSchema); } diff --git a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterValue/constants.ts b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterValue/constants.ts index e44366b03f2..8eedc70ace5 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterValue/constants.ts +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilterValue/constants.ts @@ -15,6 +15,10 @@ import { PORT_HELPER_TEXT, PORT_PLACEHOLDER_TEXT, PORTS_PLACEHOLDER_TEXT, + STATUS_CODE_HELPER_TEXT, + STATUS_CODE_PLACEHOLDER_TEXT, + STATUS_CODES_HELPER_TEXT, + STATUS_CODES_PLACEHOLDER_TEXT, VIP_HELPER_TEXT, VIP_PLACEHOLDER_TEXT, } from '../../../constants'; @@ -366,6 +370,34 @@ export const valueFieldConfig: ValueFieldConfigMap = { inputType: 'text', }, }, + status_code: { + eq_neq: { + type: 'textfield', + inputType: 'number', + min: 0, + max: Number.MAX_SAFE_INTEGER, + placeholder: STATUS_CODE_PLACEHOLDER_TEXT, + helperText: STATUS_CODE_HELPER_TEXT, + }, + startswith_endswith: { + type: 'textfield', + inputType: 'number', + min: 0, + max: Number.MAX_SAFE_INTEGER, + placeholder: STATUS_CODE_PLACEHOLDER_TEXT, + helperText: STATUS_CODE_HELPER_TEXT, + }, + in: { + type: 'textfield', + inputType: 'text', + placeholder: STATUS_CODES_PLACEHOLDER_TEXT, + helperText: STATUS_CODES_HELPER_TEXT, + }, + '*': { + type: 'textfield', + inputType: 'number', + }, + }, emptyValue: { eq_neq: { type: 'textfield', diff --git a/packages/manager/src/features/CloudPulse/Alerts/constants.ts b/packages/manager/src/features/CloudPulse/Alerts/constants.ts index 4a8876f5407..7ac7d64f669 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/constants.ts +++ b/packages/manager/src/features/CloudPulse/Alerts/constants.ts @@ -1,7 +1,5 @@ import type { FieldPath } from 'react-hook-form'; -import { PORTS_HELPER_TEXT } from '../Utils/constants'; - import type { CreateAlertDefinitionForm } from './CreateAlert/types'; import type { AlertDefinitionScope, @@ -277,27 +275,17 @@ export const CONFIGS_ID_PLACEHOLDER_TEXT = 'e.g., 1234,5678'; export const INTERFACE_ID_ERROR_MESSAGE = 'Enter a valid interface ID number.'; export const INTERFACE_ID_HELPER_TEXT = 'Enter an interface ID number.'; -export const PLACEHOLDER_TEXT_MAP: Record> = { - port: { - in: PORTS_PLACEHOLDER_TEXT, - default: PORT_PLACEHOLDER_TEXT, - }, - config_id: { - in: CONFIGS_ID_PLACEHOLDER_TEXT, - default: CONFIG_ID_PLACEHOLDER_TEXT, - }, -}; -export const HELPER_TEXT_MAP: Record> = { - port: { - in: PORTS_HELPER_TEXT, - default: PORT_HELPER_TEXT, - }, - config_id: { - in: CONFIGS_HELPER_TEXT, - default: CONFIG_ERROR_MESSAGE, - }, -}; +export const STATUS_CODE_PLACEHOLDER_TEXT = 'e.g., 200'; +export const STATUS_CODES_PLACEHOLDER_TEXT = 'e.g., 200,403,500'; + +export const STATUS_CODE_HELPER_TEXT = 'Enter a status code number.'; +export const STATUS_CODES_HELPER_TEXT = + 'Enter one or more status codes separated by commas.'; + +export const STATUS_CODE_ERROR_MESSAGE = 'Enter a valid status code number.'; +export const STATUS_CODES_ERROR_MESSAGE = + 'Enter valid status codes as integers separated by commas.'; export const entityLabelMap = { linode: 'Linode', diff --git a/packages/manager/src/features/CloudPulse/Utils/models.ts b/packages/manager/src/features/CloudPulse/Utils/models.ts index 4252612aff5..5743e1f0ea2 100644 --- a/packages/manager/src/features/CloudPulse/Utils/models.ts +++ b/packages/manager/src/features/CloudPulse/Utils/models.ts @@ -12,6 +12,7 @@ import type { NetworkLoadBalancer, NodeBalancer, ObjectStorageBucket, + Stream, Volume, } from '@linode/api-v4'; import type { QueryFunction, QueryKey } from '@tanstack/react-query'; @@ -68,6 +69,7 @@ export type QueryFunctionType = | NetworkLoadBalancer[] | NodeBalancer[] | ObjectStorageBucket[] + | Stream[] | Volume[]; /** * The non array types of QueryFunctionType like DatabaseEngine|DatabaseType diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 2ab6f042881..dfe363ddbaf 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -85,6 +85,7 @@ import { lkeEnterpriseTypeFactory, lkeHighAvailabilityTypeFactory, lkeStandardAvailabilityTypeFactory, + logsMetricCriteria, longviewActivePlanFactory, longviewClientFactory, longviewSubscriptionFactory, @@ -124,6 +125,7 @@ import { serviceTypesFactory, stackScriptFactory, staticObjects, + streamFactory, subnetFactory, supportReplyFactory, supportTicketFactory, @@ -3886,6 +3888,12 @@ export const handlers = [ regions: 'us-iad,us-east,eu-west', alert: serviceAlertFactory.build({ scope: ['entity'] }), }), + serviceTypesFactory.build({ + label: 'Logs', + service_type: 'logs', + regions: undefined, + alert: serviceAlertFactory.build({ scope: ['entity'] }), + }), ], }; @@ -3902,6 +3910,7 @@ export const handlers = [ blockstorage: 'Volumes', lke: 'LKE Enterprise', netloadbalancer: 'Network Load Balancers', + logs: 'Logs', }; const response = serviceTypesFactory.build({ service_type: `${serviceType}`, @@ -4306,6 +4315,9 @@ export const handlers = [ if (params.serviceType === 'netloadbalancer') { return HttpResponse.json({ data: networkLoadBalancerMetricCriteria }); } + if (params.serviceType === 'logs') { + return HttpResponse.json({ data: logsMetricCriteria }); + } return HttpResponse.json(response); } ), @@ -4709,6 +4721,9 @@ export const handlers = [ }, }); }), + http.get('*/monitor/streams', () => { + return HttpResponse.json(makeResourcePage(streamFactory.buildList(10))); + }), ...entityTransfers, ...statusPage, ...databases, diff --git a/packages/manager/src/queries/cloudpulse/queries.ts b/packages/manager/src/queries/cloudpulse/queries.ts index 0b8c56e05ec..7ac0ca73537 100644 --- a/packages/manager/src/queries/cloudpulse/queries.ts +++ b/packages/manager/src/queries/cloudpulse/queries.ts @@ -10,6 +10,7 @@ import { } from '@linode/api-v4'; import { databaseQueries, + deliveryQueries, firewallQueries, getAllLinodesRequest, networkLoadBalancerQueries, @@ -142,6 +143,8 @@ export const queryFactory = createQueryKeys(key, { }; case 'lke': return kubernetesQueries.lists._ctx.all; + case 'logs': + return deliveryQueries.streams._ctx.all(params, filters); case 'netloadbalancer': return networkLoadBalancerQueries.netloadbalancers._ctx.all( params, From a167f1a1d485d2db63e4327158f26320c82ae1d1 Mon Sep 17 00:00:00 2001 From: santoshp210-akamai <159890961+santoshp210-akamai@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:59:57 +0530 Subject: [PATCH 2/4] add mocks for easier testing --- .../src/factories/cloudpulse/alerts.ts | 24 +++++++++++++++++++ packages/manager/src/mocks/serverHandlers.ts | 20 ++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/packages/manager/src/factories/cloudpulse/alerts.ts b/packages/manager/src/factories/cloudpulse/alerts.ts index 2a67abc55bc..92af2ac38e2 100644 --- a/packages/manager/src/factories/cloudpulse/alerts.ts +++ b/packages/manager/src/factories/cloudpulse/alerts.ts @@ -772,3 +772,27 @@ export const logsMetricCriteria: MetricDefinition[] = [ dimensions: logsDimensions, }, ]; + +export const logsAlertMetricCriteria = + Factory.Sync.makeFactory({ + label: 'Successful Upload Count', + metric: 'success_upload_count', + unit: 'Count', + aggregate_function: 'sum', + operator: 'eq', + threshold: 1500, + dimension_filters: [ + { + label: 'Status Code', + dimension_label: 'status_code', + operator: 'in', + value: '203,402', + }, + { + label: 'Status Code', + dimension_label: 'status_code', + operator: 'eq', + value: '503', + }, + ], + }); diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index dfe363ddbaf..77605cf887b 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -85,6 +85,7 @@ import { lkeEnterpriseTypeFactory, lkeHighAvailabilityTypeFactory, lkeStandardAvailabilityTypeFactory, + logsAlertMetricCriteria, logsMetricCriteria, longviewActivePlanFactory, longviewClientFactory, @@ -3515,6 +3516,12 @@ export const handlers = [ rules: [firewallMetricRulesFactory.build()], }, }), + alertFactory.build({ + id: 494, + label: 'Logs-alert', + service_type: 'logs', + type: 'user', + }), ...alertFactory.buildList(3, { status: 'enabling', type: 'user' }), ...alertFactory.buildList(3, { status: 'disabling', type: 'user' }), ...alertFactory.buildList(3, { status: 'provisioning', type: 'user' }), @@ -3617,6 +3624,19 @@ export const handlers = [ }) ); } + if (params.id === '494' && params.serviceType === 'logs') { + return HttpResponse.json( + alertFactory.build({ + id: 494, + label: 'Logs-alert', + service_type: 'logs', + type: 'user', + rule_criteria: { + rules: [logsAlertMetricCriteria.build()], + }, + }) + ); + } if (params.id !== undefined) { return HttpResponse.json( alertFactory.build({ From bcb72fef894f7ed7e405ef31f0f1a55a64fe8fe5 Mon Sep 17 00:00:00 2001 From: santoshp210-akamai <159890961+santoshp210-akamai@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:12:34 +0530 Subject: [PATCH 3/4] add changesets --- .../.changeset/pr-13445-upcoming-features-1772199727781.md | 5 +++++ .../.changeset/pr-13445-upcoming-features-1772199672997.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 packages/api-v4/.changeset/pr-13445-upcoming-features-1772199727781.md create mode 100644 packages/manager/.changeset/pr-13445-upcoming-features-1772199672997.md diff --git a/packages/api-v4/.changeset/pr-13445-upcoming-features-1772199727781.md b/packages/api-v4/.changeset/pr-13445-upcoming-features-1772199727781.md new file mode 100644 index 00000000000..13779090c25 --- /dev/null +++ b/packages/api-v4/.changeset/pr-13445-upcoming-features-1772199727781.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Upcoming Features +--- + +Add logs to `CloudPulseServiceType` and `capabilityServiceTypeMapping` ([#13445](https://github.com/linode/manager/pull/13445)) diff --git a/packages/manager/.changeset/pr-13445-upcoming-features-1772199672997.md b/packages/manager/.changeset/pr-13445-upcoming-features-1772199672997.md new file mode 100644 index 00000000000..12637196e19 --- /dev/null +++ b/packages/manager/.changeset/pr-13445-upcoming-features-1772199672997.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Integrate aclp-logs service to alerts with custom validation schemas, error texts ([#13445](https://github.com/linode/manager/pull/13445)) From 0ca054198d32104f8e9559cc3a019ad079ca2c27 Mon Sep 17 00:00:00 2001 From: santoshp210-akamai <159890961+santoshp210-akamai@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:19:57 +0530 Subject: [PATCH 4/4] fix lint issues --- .../Alerts/AlertsResources/AlertsResources.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.tsx index 4d14cb16ad3..f2a7443d523 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.tsx @@ -179,8 +179,8 @@ export const AlertResources = React.memo((props: AlertResourcesProp) => { const filteredTypes = alertClass === 'shared' ? Object.keys(databaseTypeClassMap).filter( - (type) => type !== 'dedicated' - ) + (type) => type !== 'dedicated' + ) : [alertClass]; // Apply type filter only for DBaaS user alerts with a valid alertClass based on above filtered types @@ -206,9 +206,9 @@ export const AlertResources = React.memo((props: AlertResourcesProp) => { } = useResourcesQuery( Boolean( serviceType && - (serviceType === 'firewall' || - serviceType === 'logs' || - supportedRegionIds?.length) + (serviceType === 'firewall' || + serviceType === 'logs' || + supportedRegionIds?.length) ), // Enable query only if serviceType and supportedRegionIds are available, in case of firewall only serviceType is needed serviceType, {}, @@ -471,8 +471,8 @@ export const AlertResources = React.memo((props: AlertResourcesProp) => { new Set( regionFilteredResources ? regionFilteredResources.flatMap( - ({ tags }) => tags ?? [] - ) + ({ tags }) => tags ?? [] + ) : [] ) ),