Skip to content

Commit

Permalink
feat(amazon): support ALBRequestCountPerTarget scaling policies (#10160)
Browse files Browse the repository at this point in the history
* feat(amazon): support ALBRequestCountPerTarget scaling policies

The UI for TargetTracking scaling policies didn't support ALBRequestCountPerTarget. This was the only missing metric documented here: https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-scaling-target-tracking.html

This requires setting the resourceLabel parameter which is a combination of part of both the ALB and target group ARNs. This metric also only works with the Sum statistic. I found that the chart component hard coded average so that had to be fixed up as well.

* add target group label

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
chris-h-phillips and mergify[bot] authored Jan 22, 2025
1 parent 3005516 commit c6b3c0c
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 10 deletions.
7 changes: 6 additions & 1 deletion packages/amazon/src/domain/ITargetTrackingPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export interface ICustomizedMetricSpecification {

export interface IPredefinedMetricSpecification {
predefinedMetricType: PredefinedMetricType;
resourceLabel?: string;
}

export type PredefinedMetricType = 'ASGAverageCPUUtilization' | 'ASGAverageNetworkIn' | 'ASGAverageNetworkOut';
export type PredefinedMetricType =
| 'ASGAverageCPUUtilization'
| 'ASGAverageNetworkIn'
| 'ASGAverageNetworkOut'
| 'ALBRequestCountPerTarget';
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { get } from 'lodash';
import * as React from 'react';

import type { ICloudMetricStatistics } from '@spinnaker/core';
Expand Down Expand Up @@ -36,7 +37,7 @@ export function MetricAlarmChartImpl(props: IMetricAlarmChartProps) {
return result;
},
{ datapoints: [], unit: '' },
[namespace, statistic, period, type, account, region, metricName],
[namespace, statistic, period, type, account, region, metricName, alarm.dimensions],
);

if (status === 'PENDING') {
Expand All @@ -60,13 +61,15 @@ export function MetricAlarmChartImpl(props: IMetricAlarmChartProps) {

const now = new Date();
const oneDayAgo = new Date(Date.now() - 1000 * 60 * 60 * 24);

const line: IDateLine = {
label: metricName,
fill: 'stack',
borderColor: 'green',
borderWidth: 2,
data: result.datapoints.map((dp) => ({ x: new Date(dp.timestamp), y: dp.average })),
data: result.datapoints.map((dp) => ({
x: new Date(dp.timestamp),
y: get(dp, [alarm.statistic.toLowerCase()], undefined),
})),
};

const setline: IDateLine = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@ import { cloneDeep, set } from 'lodash';
import * as React from 'react';

import { NumberInput, ReactSelectInput } from '@spinnaker/core';
import type { Application, ILoadBalancer } from '@spinnaker/core';

import type { ITargetTrackingPolicyCommand } from '../ScalingPolicyWriter';
import { TargetTrackingChart } from './TargetTrackingChart';
import type { IAmazonServerGroup, ICustomizedMetricSpecification, IScalingPolicyAlarmView } from '../../../../domain';
import type {
IAmazonApplicationLoadBalancer,
IAmazonServerGroup,
ICustomizedMetricSpecification,
IScalingPolicyAlarmView,
ITargetGroup,
} from '../../../../domain';
import { MetricSelector } from '../upsert/alarm/MetricSelector';

import './TargetMetricFields.less';
Expand All @@ -16,21 +23,36 @@ export interface ITargetMetricFieldsProps {
cloudwatch?: boolean;
command: ITargetTrackingPolicyCommand;
isCustomMetric: boolean;
app: Application;
serverGroup: IAmazonServerGroup;
toggleMetricType?: (type: MetricType) => void;
updateCommand: (command: ITargetTrackingPolicyCommand) => void;
}

interface IalbArn {
loadBalancerArn: string;
}

interface ItargetGroupArn {
targetGroupArn: string;
}

export const TargetMetricFields = ({
allowDualMode,
cloudwatch,
command,
isCustomMetric,
app,
serverGroup,
toggleMetricType,
updateCommand,
}: ITargetMetricFieldsProps) => {
const predefinedMetrics = ['ASGAverageCPUUtilization', 'ASGAverageNetworkOut', 'ASGAverageNetworkIn'];
const predefinedMetrics = [
'ASGAverageCPUUtilization',
'ASGAverageNetworkOut',
'ASGAverageNetworkIn',
'ALBRequestCountPerTarget',
];
const statistics = ['Average', 'Maximum', 'Minimum', 'SampleCount', 'Sum'];
const [unit, setUnit] = React.useState<string>(null);

Expand Down Expand Up @@ -65,6 +87,24 @@ export const TargetMetricFields = ({
toggleMetricType(isCustomMetric ? 'predefined' : 'custom');
};

const targetGroupOptions = () => {
const loadBalancers = app.loadBalancers.data as ILoadBalancer[];
const albs = loadBalancers.filter(
(lb) => lb.account === serverGroup.account && lb.region === serverGroup.region,
) as Array<IAmazonApplicationLoadBalancer & IalbArn>;
const targetGroups = albs.flatMap((alb) =>
alb.targetGroups
.filter((tg) => serverGroup.targetGroups.some((serverGroupTg) => serverGroupTg === tg.name))
.map((tg) => ({ ...tg, loadBalancerArn: alb.loadBalancerArn })),
) as Array<ITargetGroup & IalbArn & ItargetGroupArn>;
return targetGroups.map((tg) => ({
label: tg.name,
value: `${tg.loadBalancerArn.substring(tg.loadBalancerArn.indexOf('app'))}/${tg.targetGroupArn.substring(
tg.targetGroupArn.indexOf('targetgroup'),
)}`,
}));
};

return (
<div className="TargetMetricFields sp-margin-l-xaxis">
<p>
Expand Down Expand Up @@ -94,6 +134,7 @@ export const TargetMetricFields = ({
inputClassName="metric-select-input"
/>
)}

{isCustomMetric && (
<MetricSelector
alarm={command.targetTrackingConfiguration.customizedMetricSpecification as IScalingPolicyAlarmView}
Expand All @@ -108,6 +149,26 @@ export const TargetMetricFields = ({
)}
</div>
</div>
{!isCustomMetric &&
command.targetTrackingConfiguration.predefinedMetricSpecification?.predefinedMetricType ===
'ALBRequestCountPerTarget' && (
<div className="row sp-margin-s-yaxis">
<div className="col-md-2 sm-label-right">Target Group</div>
<div className="col-md-10 content-fields horizontal">
<ReactSelectInput
value={command.targetTrackingConfiguration.predefinedMetricSpecification?.resourceLabel}
options={targetGroupOptions()}
onChange={(e) =>
setCommandField(
'targetTrackingConfiguration.predefinedMetricSpecification.resourceLabel',
e.target.value,
)
}
inputClassName="metric-select-input"
/>
</div>
</div>
)}
<div className="row sp-margin-s-yaxis">
<div className="col-md-2 sm-label-right">Target</div>
<div className="col-md-10 content-fields horizontal">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const predefinedMetricTypeMapping: Dictionary<string> = {
ASGAverageCPUUtilization: 'CPUUtilization',
ASGAverageNetworkIn: 'NetworkIn',
ASGAverageNetworkOut: 'NetworkOut',
ALBRequestCountPerTarget: 'RequestCountPerTarget',
};

export const TargetTrackingChart = ({ config, serverGroup, updateUnit }: ITargetTrackingChartProps) => {
Expand All @@ -40,12 +41,11 @@ export const TargetTrackingChart = ({ config, serverGroup, updateUnit }: ITarget

const synchronizeAlarm = () => {
const customMetric = config?.customizedMetricSpecification;
const predefMetric = config?.predefinedMetricSpecification;
const updatedAlarm = {
...alarm,
dimensions: customMetric?.dimensions || [{ name: 'AutoScalingGroupName', value: serverGroup.name }],
metricName:
customMetric?.metricName ||
predefinedMetricTypeMapping[config?.predefinedMetricSpecification?.predefinedMetricType],
metricName: customMetric?.metricName || predefinedMetricTypeMapping[predefMetric?.predefinedMetricType],
namespace: customMetric?.namespace || 'AWS/EC2',
threshold: config?.targetValue,
};
Expand All @@ -54,6 +54,20 @@ export const TargetTrackingChart = ({ config, serverGroup, updateUnit }: ITarget
updatedAlarm.statistic = customMetric?.statistic;
}

if (predefMetric && predefMetric.predefinedMetricType === 'ALBRequestCountPerTarget') {
updatedAlarm.statistic = 'Sum';
updatedAlarm.namespace = 'AWS/ApplicationELB';
if (predefMetric?.resourceLabel) {
const parts = predefMetric?.resourceLabel.split('/');
const loadBalancer = parts.slice(0, 3).join('/');
const targetGroup = parts.slice(3).join('/');
updatedAlarm.dimensions = [
{ name: 'LoadBalancer', value: loadBalancer },
{ name: 'TargetGroup', value: targetGroup },
];
}
}

setAlarm(updatedAlarm);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const UpsertTargetTrackingModal = ({
cloudwatch={false}
command={command as ITargetTrackingPolicyCommand}
isCustomMetric={isCustom}
app={app}
serverGroup={serverGroup}
toggleMetricType={(t) => setIsCustom(t === 'custom')}
updateCommand={setCommand}
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/domain/ICloudMetric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ export interface IMetricAlarmDimension {

interface IDataPoint {
timestamp: number;
average: number;
average?: number;
sum?: number;
minimum?: number;
maximum?: number;
sampleCount?: number;
}

export interface ICloudMetricStatistics {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const UpsertTargetTrackingModal = ({
cloudwatch={true}
command={command as ITargetTrackingPolicyCommand}
isCustomMetric={isCustom}
app={app}
serverGroup={(serverGroup as unknown) as IAmazonServerGroup}
toggleMetricType={(t) => setIsCustom(t === 'custom')}
updateCommand={setCommand}
Expand Down

0 comments on commit c6b3c0c

Please sign in to comment.