Skip to content

Commit

Permalink
feat(elbv2): add metrics to INetworkLoadBalancer
Browse files Browse the repository at this point in the history
fix: #10850

By adding the metrics methods to the `INetworkLoadBalancer` interface, it allows
to create these metrics also for NLBs that are imported via the `fromXXX`
methods.

Given that to create the metrics for NLBs, only the full name of the NLB is
required, and this attribute is available at the constructs returned by the
`fromXXX` methods.

To solve this problem, it was just a matter of reorganizing the code a bit and
adding a delegate that implements the metrics themselves.
  • Loading branch information
Gustavo Muenz committed Jan 26, 2023
1 parent 660198b commit 3ba1aba
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
import { Resource } from '@aws-cdk/core';
import { Arn, ArnFormat, Resource, ResourceProps } from '@aws-cdk/core';
import * as cxapi from '@aws-cdk/cx-api';
import { Construct } from 'constructs';
import { NetworkELBMetrics } from '../elasticloadbalancingv2-canned-metrics.generated';
import { BaseLoadBalancer, BaseLoadBalancerLookupOptions, BaseLoadBalancerProps, ILoadBalancerV2 } from '../shared/base-load-balancer';
import { Ctor, delegate } from '../shared/util';
import { BaseNetworkListenerProps, NetworkListener } from './network-listener';

/**
Expand Down Expand Up @@ -58,12 +59,114 @@ export interface NetworkLoadBalancerAttributes {
export interface NetworkLoadBalancerLookupOptions extends BaseLoadBalancerLookupOptions {
}

const METRIC_METHODS: string[] = [
'metric',
'metricActiveFlowCount',
'metricConsumedLCUs',
'metricHealthyHostCount',
'metricUnHealthyHostCount',
'metricNewFlowCount',
'metricProcessedBytes',
'metricTcpClientResetCount',
'metricTcpElbResetCount',
'metricTcpTargetResetCount',
];

/**
* Helper class that implements metrics for a network load balancer.
*
* It can be used as a delegate
*/
class NetworkLoadBalancerMetrics implements INetworkLoadBalancerMetrics {
private readonly loadBalancerFullName: string;
private readonly resource: Resource;

constructor(resource: Resource, loadBalancerArn: string) {
this.resource = resource;
const arnComponents = Arn.split(loadBalancerArn, ArnFormat.SLASH_RESOURCE_NAME);
if (!arnComponents.resourceName) {
throw new Error(`Provided ARN does not belong to a load balancer: ${loadBalancerArn}`);
}
this.loadBalancerFullName = arnComponents.resourceName;
}

public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric {
return new cloudwatch.Metric({
namespace: 'AWS/NetworkELB',
metricName,
dimensions: { LoadBalancer: this.loadBalancerFullName },
...props,
}).attachTo(this.resource);
}

public metricActiveFlowCount(props?: cloudwatch.MetricOptions) {
return this.cannedMetric(NetworkELBMetrics.activeFlowCountAverage, props);
}

public metricConsumedLCUs(props?: cloudwatch.MetricOptions) {
return this.cannedMetric(NetworkELBMetrics.consumedLcUsAverage, {
statistic: 'Sum',
...props,
});
}

public metricHealthyHostCount(props?: cloudwatch.MetricOptions) {
return this.metric('HealthyHostCount', {
statistic: 'Average',
...props,
});
}

public metricUnHealthyHostCount(props?: cloudwatch.MetricOptions) {
return this.metric('UnHealthyHostCount', {
statistic: 'Average',
...props,
});
}

public metricNewFlowCount(props?: cloudwatch.MetricOptions) {
return this.cannedMetric(NetworkELBMetrics.newFlowCountSum, props);
}

public metricProcessedBytes(props?: cloudwatch.MetricOptions) {
return this.cannedMetric(NetworkELBMetrics.processedBytesSum, props);
}

public metricTcpClientResetCount(props?: cloudwatch.MetricOptions) {
return this.cannedMetric(NetworkELBMetrics.tcpClientResetCountSum, props);
}
public metricTcpElbResetCount(props?: cloudwatch.MetricOptions) {
return this.cannedMetric(NetworkELBMetrics.tcpElbResetCountSum, props);
}
public metricTcpTargetResetCount(props?: cloudwatch.MetricOptions) {
return this.cannedMetric(NetworkELBMetrics.tcpTargetResetCountSum, props);
}

private cannedMetric(
fn: (dims: { LoadBalancer: string }) => cloudwatch.MetricProps,
props?: cloudwatch.MetricOptions,
): cloudwatch.Metric {
return new cloudwatch.Metric({
...fn({ LoadBalancer: this.loadBalancerFullName }),
...props,
}).attachTo(this.resource);
}
}

const DelegatedMetricsAndExtendBaseLoadBalancer: Ctor<INetworkLoadBalancerMetrics> & Ctor<BaseLoadBalancer> = delegate(
{ to: '_metrics', methods: METRIC_METHODS as Array<keyof INetworkLoadBalancerMetrics> },
);

const DelegatedMetricsAndExtendResource: Ctor<INetworkLoadBalancerMetrics> & Ctor<Resource> = delegate(
{ to: '_metrics', methods: METRIC_METHODS as Array<keyof INetworkLoadBalancerMetrics> },
);

/**
* Define a new network load balancer
*
* @resource AWS::ElasticLoadBalancingV2::LoadBalancer
*/
export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoadBalancer {
export class NetworkLoadBalancer extends DelegatedMetricsAndExtendBaseLoadBalancer implements INetworkLoadBalancer {
/**
* Looks up the network load balancer.
*/
Expand All @@ -77,9 +180,18 @@ export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoa
}

public static fromNetworkLoadBalancerAttributes(scope: Construct, id: string, attrs: NetworkLoadBalancerAttributes): INetworkLoadBalancer {
class Import extends Resource implements INetworkLoadBalancer {

class Import extends DelegatedMetricsAndExtendResource implements INetworkLoadBalancer {
public readonly loadBalancerArn = attrs.loadBalancerArn;
public readonly vpc?: ec2.IVpc = attrs.vpc;
// @ts-ignore
private readonly _metrics: NetworkLoadBalancerMetrics;

constructor(scope: Construct, id: string, props: ResourceProps) {
super(scope, id, props);
this._metrics = new NetworkLoadBalancerMetrics(this, this.loadBalancerArn);
}

public addListener(lid: string, props: BaseNetworkListenerProps): NetworkListener {
return new NetworkListener(this, lid, {
loadBalancer: this,
Expand All @@ -103,11 +215,15 @@ export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoa
return new Import(scope, id, { environmentFromArn: attrs.loadBalancerArn });
}

// @ts-ignore
private readonly _metrics: NetworkLoadBalancerMetrics;

constructor(scope: Construct, id: string, props: NetworkLoadBalancerProps) {
super(scope, id, props, {
type: 'network',
});

this._metrics = new NetworkLoadBalancerMetrics(this, this.loadBalancerArn);
if (props.crossZoneEnabled) { this.setAttribute('load_balancing.cross_zone.enabled', 'true'); }
}

Expand All @@ -122,20 +238,16 @@ export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoa
...props,
});
}
}

export interface INetworkLoadBalancerMetrics {

/**
* Return the given named metric for this Network Load Balancer
*
* @default Average over 5 minutes
*/
public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric {
return new cloudwatch.Metric({
namespace: 'AWS/NetworkELB',
metricName,
dimensions: { LoadBalancer: this.loadBalancerFullName },
...props,
}).attachTo(this);
}
metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric;

/**
* The total number of concurrent TCP flows (or connections) from clients to targets.
Expand All @@ -146,65 +258,44 @@ export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoa
*
* @default Average over 5 minutes
*/
public metricActiveFlowCount(props?: cloudwatch.MetricOptions) {
return this.cannedMetric(NetworkELBMetrics.activeFlowCountAverage, props);
}
metricActiveFlowCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric;

/**
* The number of load balancer capacity units (LCU) used by your load balancer.
*
* @default Sum over 5 minutes
*/
public metricConsumedLCUs(props?: cloudwatch.MetricOptions) {
return this.cannedMetric(NetworkELBMetrics.consumedLcUsAverage, {
statistic: 'Sum',
...props,
});
}
metricConsumedLCUs(props?: cloudwatch.MetricOptions): cloudwatch.Metric;

/**
* The number of targets that are considered healthy.
*
* @default Average over 5 minutes
* @deprecated use ``NetworkTargetGroup.metricHealthyHostCount`` instead
*/
public metricHealthyHostCount(props?: cloudwatch.MetricOptions) {
return this.metric('HealthyHostCount', {
statistic: 'Average',
...props,
});
}
metricHealthyHostCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric;

/**
* The number of targets that are considered unhealthy.
*
* @default Average over 5 minutes
* @deprecated use ``NetworkTargetGroup.metricUnHealthyHostCount`` instead
*/
public metricUnHealthyHostCount(props?: cloudwatch.MetricOptions) {
return this.metric('UnHealthyHostCount', {
statistic: 'Average',
...props,
});
}
metricUnHealthyHostCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric;

/**
* The total number of new TCP flows (or connections) established from clients to targets in the time period.
*
* @default Sum over 5 minutes
*/
public metricNewFlowCount(props?: cloudwatch.MetricOptions) {
return this.cannedMetric(NetworkELBMetrics.newFlowCountSum, props);
}
metricNewFlowCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric;

/**
* The total number of bytes processed by the load balancer, including TCP/IP headers.
*
* @default Sum over 5 minutes
*/
public metricProcessedBytes(props?: cloudwatch.MetricOptions) {
return this.cannedMetric(NetworkELBMetrics.processedBytesSum, props);
}
metricProcessedBytes(props?: cloudwatch.MetricOptions): cloudwatch.Metric;

/**
* The total number of reset (RST) packets sent from a client to a target.
Expand All @@ -213,18 +304,14 @@ export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoa
*
* @default Sum over 5 minutes
*/
public metricTcpClientResetCount(props?: cloudwatch.MetricOptions) {
return this.cannedMetric(NetworkELBMetrics.tcpClientResetCountSum, props);
}
metricTcpClientResetCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric;

/**
* The total number of reset (RST) packets generated by the load balancer.
*
* @default Sum over 5 minutes
*/
public metricTcpElbResetCount(props?: cloudwatch.MetricOptions) {
return this.cannedMetric(NetworkELBMetrics.tcpElbResetCountSum, props);
}
metricTcpElbResetCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric;

/**
* The total number of reset (RST) packets sent from a target to a client.
Expand All @@ -233,24 +320,13 @@ export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoa
*
* @default Sum over 5 minutes
*/
public metricTcpTargetResetCount(props?: cloudwatch.MetricOptions) {
return this.cannedMetric(NetworkELBMetrics.tcpTargetResetCountSum, props);
}

private cannedMetric(
fn: (dims: { LoadBalancer: string }) => cloudwatch.MetricProps,
props?: cloudwatch.MetricOptions): cloudwatch.Metric {
return new cloudwatch.Metric({
...fn({ LoadBalancer: this.loadBalancerFullName }),
...props,
}).attachTo(this);
}
metricTcpTargetResetCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric;
}

/**
* A network load balancer
*/
export interface INetworkLoadBalancer extends ILoadBalancerV2, ec2.IVpcEndpointServiceLoadBalancer {
export interface INetworkLoadBalancer extends ILoadBalancerV2, INetworkLoadBalancerMetrics, ec2.IVpcEndpointServiceLoadBalancer {

/**
* The VPC this load balancer has been created in (if available)
Expand All @@ -265,18 +341,21 @@ export interface INetworkLoadBalancer extends ILoadBalancerV2, ec2.IVpcEndpointS
addListener(id: string, props: BaseNetworkListenerProps): NetworkListener;
}

class LookedUpNetworkLoadBalancer extends Resource implements INetworkLoadBalancer {
class LookedUpNetworkLoadBalancer extends DelegatedMetricsAndExtendResource implements INetworkLoadBalancer {
public readonly loadBalancerCanonicalHostedZoneId: string;
public readonly loadBalancerDnsName: string;
public readonly loadBalancerArn: string;
public readonly vpc?: ec2.IVpc;
// @ts-ignore
private readonly _metrics: NetworkLoadBalancerMetrics;

constructor(scope: Construct, id: string, props: cxapi.LoadBalancerContextResponse) {
super(scope, id, { environmentFromArn: props.loadBalancerArn });

this.loadBalancerArn = props.loadBalancerArn;
this.loadBalancerCanonicalHostedZoneId = props.loadBalancerCanonicalHostedZoneId;
this.loadBalancerDnsName = props.loadBalancerDnsName;
this._metrics = new NetworkLoadBalancerMetrics(this, props.loadBalancerArn);

this.vpc = ec2.Vpc.fromLookup(this, 'Vpc', {
vpcId: props.vpcId,
Expand Down
51 changes: 51 additions & 0 deletions packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,54 @@ export function mapTagMapToCxschema(tagMap: Record<string, string>): cxschema.Ta
return Object.entries(tagMap)
.map(([key, value]) => ({ key, value }));
}

export type Ctor<T> = new(...args: any[]) => T;
type DelegateParams = {to: string, methods?: string[]};

function _delegate<T extends Ctor<any>>(base: T, params: DelegateParams): Ctor<any> {
abstract class Clazz extends base {}

params.methods?.forEach(method => {
(Clazz.prototype as any)[method] = function(...args: unknown[]) {
return this[params.to][method](...args);
};
});

return Clazz;
}

/**
* Helper to easily create delegates without having to write lots of boilerplate methods.
*
* Taken from https://francium.cc/blog/2020-08-22-making-delegation-easier-in-javascript/
*
* @example
* interface INetworkLoadBalancerMetrics {
* metric();
* }
* interface INetworkLoadBalancer extends INetworkLoadBalancerMetrics {}
*
* const DelegateAndExtendResource: Ctor<INetworkLoadBalancerMetrics> & Ctor<Resource> = delegate(
* { to: '_metrics', methods: ['metric'] as Array<keyof INetworkLoadBalancerMetrics> },
* );
* class Metrics implement INetworkLoadBalancerMetrics {
* public metric() {}
* }
* class NetworkLoadBalancer extends DelegateAndExtendResource implements INetworkLoadBalancer {
* _metrics: Metrics;
* constructor(...) {
* this._metrics = new Metrics();
* }
* }
* class LookupNetworkLoadBalancer extends DelegateAndExtendResource implements INetworkLoadBalancer {
* _metrics: Metrics;
* constructor(...) {
* this._metrics = new Metrics();
* }
* }
*/
export function delegate(...targets: Array<DelegateParams>): any {
return targets.reduceRight((prev, cur) => {
return _delegate(prev, cur);
}, Object as Ctor<any>);
}

0 comments on commit 3ba1aba

Please sign in to comment.